From f9a1a6ba4f57be77cc92122ae13df3dad6d3191e Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Wed, 16 May 2018 10:34:50 -0700 Subject: [PATCH 001/373] Update changelog for release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76cce76d..0f3088da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased + +--- + +## 1.6.0 - 2018-05-16 ### Added - Connection and request timeouts could be specified also when creating RemoteWebDriver from existing session ID. - Update PHPDoc for functions that return static instances of a class. From a1784b20ea64afdb0613e5164b3cb42773df4578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 28 May 2018 16:32:37 +0200 Subject: [PATCH 002/373] Update Selenium to 3.8.1 and Chromedriver to 2.38 --- .travis.yml | 6 +++--- tests/functional/RemoteWebDriverTest.php | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3c9c5c1a3..4a2cada83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ env: global: - DISPLAY=:99.0 - BROWSER_NAME="htmlunit" - - CHROMEDRIVER_VERSION="2.35" + - CHROMEDRIVER_VERSION="2.38" matrix: include: @@ -93,8 +93,8 @@ before_script: - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - sh -e /etc/init.d/xvfb start - - if [ ! -f jar/selenium-server-standalone-3.4.0.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.4/selenium-server-standalone-3.4.0.jar; fi - - java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.4.0.jar -log ./logs/selenium.log & + - if [ ! -f jar/selenium-server-standalone-3.8.1.jar ]; then wget -q -t 3 -P jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; fi + - java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$CHROMEDRIVER_PATH" -jar jar/selenium-server-standalone-3.8.1.jar -enablePassThrough false -log ./logs/selenium.log & - until $(echo | nc localhost 4444); do sleep 1; echo Waiting for Selenium server on port 4444...; done; echo "Selenium server started" - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index 440776ad5..818a9da5a 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -84,7 +84,6 @@ public function testShouldGetAllSessions() $this->assertArrayHasKey('capabilities', $sessions[0]); $this->assertArrayHasKey('id', $sessions[0]); - $this->assertArrayHasKey('class', $sessions[0]); } /** From 6c885357678c692ee76d97d21ba03fbfbd19b46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 29 May 2018 15:03:45 +0200 Subject: [PATCH 003/373] Add utility classes to interact with checkboxes and radio buttons (#545) * Add an utility class to manipulate checkboxes and radio buttons --- lib/AbstractWebDriverCheckboxOrRadio.php | 248 ++++++++++++++++++ lib/WebDriverCheckboxes.php | 66 +++++ lib/WebDriverRadios.php | 65 +++++ tests/functional/WebDriverCheckboxesTest.php | 197 ++++++++++++++ tests/functional/WebDriverRadiosTest.php | 184 +++++++++++++ tests/functional/web/form_checkbox_radio.html | 43 +++ 6 files changed, 803 insertions(+) create mode 100644 lib/AbstractWebDriverCheckboxOrRadio.php create mode 100644 lib/WebDriverCheckboxes.php create mode 100644 lib/WebDriverRadios.php create mode 100644 tests/functional/WebDriverCheckboxesTest.php create mode 100644 tests/functional/WebDriverRadiosTest.php create mode 100644 tests/functional/web/form_checkbox_radio.html diff --git a/lib/AbstractWebDriverCheckboxOrRadio.php b/lib/AbstractWebDriverCheckboxOrRadio.php new file mode 100644 index 000000000..46288592b --- /dev/null +++ b/lib/AbstractWebDriverCheckboxOrRadio.php @@ -0,0 +1,248 @@ +getTagName(); + if ($tagName !== 'input') { + throw new UnexpectedTagNameException('input', $tagName); + } + + $this->name = $element->getAttribute('name'); + if ($this->name === null) { + throw new WebDriverException('The input does not have a "name" attribute.'); + } + + $this->element = $element; + } + + public function getOptions() + { + return $this->getRelatedElements(); + } + + public function getAllSelectedOptions() + { + $selectedElement = []; + foreach ($this->getRelatedElements() as $element) { + if ($element->isSelected()) { + $selectedElement[] = $element; + + if (!$this->isMultiple()) { + return $selectedElement; + } + } + } + + return $selectedElement; + } + + public function getFirstSelectedOption() + { + foreach ($this->getRelatedElements() as $element) { + if ($element->isSelected()) { + return $element; + } + } + + throw new NoSuchElementException( + sprintf('No %s are selected', 'radio' === $this->type ? 'radio buttons' : 'checkboxes') + ); + } + + public function selectByIndex($index) + { + $this->byIndex($index); + } + + public function selectByValue($value) + { + $this->byValue($value); + } + + public function selectByVisibleText($text) + { + $this->byVisibleText($text); + } + + public function selectByVisiblePartialText($text) + { + $this->byVisibleText($text, true); + } + + /** + * Selects or deselects a checkbox or a radio button by its value. + * + * @param string $value + * @param bool $select + * @throws NoSuchElementException + */ + protected function byValue($value, $select = true) + { + $matched = false; + foreach ($this->getRelatedElements($value) as $element) { + $select ? $this->selectOption($element) : $this->deselectOption($element); + if (!$this->isMultiple()) { + return; + } + + $matched = true; + } + + if (!$matched) { + throw new NoSuchElementException( + sprintf('Cannot locate %s with value: %s', $this->type, $value) + ); + } + } + + /** + * Selects or deselects a checkbox or a radio button by its index. + * + * @param int $index + * @param bool $select + * @throws NoSuchElementException + */ + protected function byIndex($index, $select = true) + { + $elements = $this->getRelatedElements(); + if (!isset($elements[$index])) { + throw new NoSuchElementException(sprintf('Cannot locate %s with index: %d', $this->type, $index)); + } + + $select ? $this->selectOption($elements[$index]) : $this->deselectOption($elements[$index]); + } + + /** + * Selects or deselects a checkbox or a radio button by its visible text. + * + * @param string $text + * @param bool $partial + * @param bool $select + */ + protected function byVisibleText($text, $partial = false, $select = true) + { + foreach ($this->getRelatedElements() as $element) { + $normalizeFilter = sprintf( + $partial ? 'contains(normalize-space(.), %s)' : 'normalize-space(.) = %s', + XPathEscaper::escapeQuotes($text) + ); + + $xpath = 'ancestor::label'; + $xpathNormalize = sprintf('%s[%s]', $xpath, $normalizeFilter); + + $id = $element->getAttribute('id'); + if ($id !== null) { + $idFilter = sprintf('@for = %s', XPathEscaper::escapeQuotes($id)); + + $xpath .= sprintf(' | //label[%s]', $idFilter); + $xpathNormalize .= sprintf(' | //label[%s and %s]', $idFilter, $normalizeFilter); + } + + try { + $element->findElement(WebDriverBy::xpath($xpathNormalize)); + } catch (NoSuchElementException $e) { + if ($partial) { + continue; + } + + try { + // Since the mechanism of getting the text in xpath is not the same as + // webdriver, use the expensive getText() to check if nothing is matched. + if ($text !== $element->findElement(WebDriverBy::xpath($xpath))->getText()) { + continue; + } + } catch (NoSuchElementException $e) { + continue; + } + } + + $select ? $this->selectOption($element) : $this->deselectOption($element); + if (!$this->isMultiple()) { + return; + } + } + } + + /** + * Gets checkboxes or radio buttons with the same name. + * + * @param string|null $value + * @return WebDriverElement[] + */ + protected function getRelatedElements($value = null) + { + $valueSelector = $value ? sprintf(' and @value = %s', XPathEscaper::escapeQuotes($value)) : ''; + $formId = $this->element->getAttribute('form'); + if ($formId === null) { + $form = $this->element->findElement(WebDriverBy::xpath('ancestor::form')); + + $formId = $form->getAttribute('id'); + if ($formId === '') { + return $form->findElements(WebDriverBy::xpath( + sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector) + )); + } + } + + return $this->element->findElements(WebDriverBy::xpath(sprintf( + '//form[@id = %1$s]//input[@name = %2$s%3$s] | //input[@form = %1$s and @name = %2$s%3$s]', + XPathEscaper::escapeQuotes($formId), + XPathEscaper::escapeQuotes($this->name), + $valueSelector + ))); + } + + /** + * Selects a checkbox or a radio button. + */ + protected function selectOption(WebDriverElement $element) + { + if (!$element->isSelected()) { + $element->click(); + } + } + + /** + * Deselects a checkbox or a radio button. + */ + protected function deselectOption(WebDriverElement $element) + { + if ($element->isSelected()) { + $element->click(); + } + } +} diff --git a/lib/WebDriverCheckboxes.php b/lib/WebDriverCheckboxes.php new file mode 100644 index 000000000..1ac00093d --- /dev/null +++ b/lib/WebDriverCheckboxes.php @@ -0,0 +1,66 @@ +type = $element->getAttribute('type'); + if ($this->type !== 'checkbox') { + throw new WebDriverException('The input must be of type "checkbox".'); + } + } + + public function isMultiple() + { + return true; + } + + public function deselectAll() + { + foreach ($this->getRelatedElements() as $checkbox) { + $this->deselectOption($checkbox); + } + } + + public function deselectByIndex($index) + { + $this->byIndex($index, false); + } + + public function deselectByValue($value) + { + $this->byValue($value, false); + } + + public function deselectByVisibleText($text) + { + $this->byVisibleText($text, false, false); + } + + public function deselectByVisiblePartialText($text) + { + $this->byVisibleText($text, true, false); + } +} diff --git a/lib/WebDriverRadios.php b/lib/WebDriverRadios.php new file mode 100644 index 000000000..d6a4d2ac7 --- /dev/null +++ b/lib/WebDriverRadios.php @@ -0,0 +1,65 @@ +type = $element->getAttribute('type'); + if ($this->type !== 'radio') { + throw new WebDriverException('The input must be of type "radio".'); + } + } + + public function isMultiple() + { + return false; + } + + public function deselectAll() + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByIndex($index) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByValue($value) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByVisibleText($text) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } + + public function deselectByVisiblePartialText($text) + { + throw new UnsupportedOperationException('You cannot deselect radio buttons'); + } +} diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php new file mode 100644 index 000000000..3bf8ccffd --- /dev/null +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -0,0 +1,197 @@ +driver->get($this->getTestPageUrl('form_checkbox_radio.html')); + } + + public function testIsMultiple() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $this->assertTrue($c->isMultiple()); + } + + public function testGetOptions() + { + $c = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//form[2]//input[@type="checkbox"]')) + ); + $this->assertNotEmpty($c->getOptions()); + } + + public function testGetFirstSelectedOption() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $c->selectByValue('j2a'); + $this->assertSame('j2a', $c->getFirstSelectedOption()->getAttribute('value')); + } + + public function testSelectByValue() + { + $selectedOptions = ['j2b', 'j2c']; + + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + foreach ($selectedOptions as $index => $selectedOption) { + $c->selectByValue($selectedOption); + } + + $selectedValues = []; + foreach ($c->getAllSelectedOptions() as $option) { + $selectedValues[] = $option->getAttribute('value'); + } + $this->assertSame($selectedOptions, $selectedValues); + } + + public function testSelectByValueInvalid() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $this->expectException(NoSuchElementException::class); + $this->expectExceptionMessage('Cannot locate checkbox with value: notexist'); + $c->selectByValue('notexist'); + } + + public function testSelectByIndex() + { + $selectedOptions = [1 => 'j2b', 2 => 'j2c']; + + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + foreach ($selectedOptions as $index => $selectedOption) { + $c->selectByIndex($index); + } + + $selectedValues = []; + foreach ($c->getAllSelectedOptions() as $option) { + $selectedValues[] = $option->getAttribute('value'); + } + $this->assertSame(array_values($selectedOptions), $selectedValues); + } + + public function testSelectByIndexInvalid() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $this->expectException(NoSuchElementException::class); + $this->expectExceptionMessage('Cannot locate checkbox with index: ' . PHP_INT_MAX); + $c->selectByIndex(PHP_INT_MAX); + } + + /** + * @dataProvider selectByVisibleTextDataProvider + * + * @param string $text + * @param string $value + */ + public function testSelectByVisibleText($text, $value) + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $c->selectByVisibleText($text); + $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + } + + /** + * @return array + */ + public function selectByVisibleTextDataProvider() + { + return [ + ['J 2 B', 'j2b'], + ['J2C', 'j2c'], + ]; + } + + /** + * @dataProvider selectByVisiblePartialTextDataProvider + * + * @param string $text + * @param string $value + */ + public function testSelectByVisiblePartialText($text, $value) + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $c->selectByVisiblePartialText($text); + $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + } + + /** + * @return array + */ + public function selectByVisiblePartialTextDataProvider() + { + return [ + ['2 B', 'j2b'], + ['2C', 'j2c'], + ]; + } + + public function testDeselectAll() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $c->selectByIndex(0); + $this->assertCount(1, $c->getAllSelectedOptions()); + $c->deselectAll(); + $this->assertEmpty($c->getAllSelectedOptions()); + } + + public function testDeselectByIndex() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $c->selectByIndex(0); + $this->assertCount(1, $c->getAllSelectedOptions()); + $c->deselectByIndex(0); + $this->assertEmpty($c->getAllSelectedOptions()); + } + + public function testDeselectByValue() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $c->selectByValue('j2a'); + $this->assertCount(1, $c->getAllSelectedOptions()); + $c->deselectByValue('j2a'); + $this->assertEmpty($c->getAllSelectedOptions()); + } + + public function testDeselectByVisibleText() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $c->selectByVisibleText('J 2 B'); + $this->assertCount(1, $c->getAllSelectedOptions()); + $c->deselectByVisibleText('J 2 B'); + $this->assertEmpty($c->getAllSelectedOptions()); + } + + public function testDeselectByVisiblePartialText() + { + $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + + $c->selectByVisiblePartialText('2C'); + $this->assertCount(1, $c->getAllSelectedOptions()); + $c->deselectByVisiblePartialText('2C'); + $this->assertEmpty($c->getAllSelectedOptions()); + } +} diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php new file mode 100644 index 000000000..f28417ac4 --- /dev/null +++ b/tests/functional/WebDriverRadiosTest.php @@ -0,0 +1,184 @@ +driver->get($this->getTestPageUrl('form_checkbox_radio.html')); + } + + public function testIsMultiple() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $this->assertFalse($c->isMultiple()); + } + + public function testGetOptions() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $values = []; + foreach ($c->getOptions() as $option) { + $values[] = $option->getAttribute('value'); + } + + $this->assertSame(['j3a', 'j3b', 'j3c'], $values); + } + + public function testGetFirstSelectedOption() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $c->selectByValue('j3a'); + $this->assertSame('j3a', $c->getFirstSelectedOption()->getAttribute('value')); + } + + public function testSelectByValue() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $c->selectByValue('j3b'); + + $selectedOptions = $c->getAllSelectedOptions(); + $this->assertCount(1, $selectedOptions); + $this->assertSame('j3b', $selectedOptions[0]->getAttribute('value')); + } + + public function testSelectByValueInvalid() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(NoSuchElementException::class); + $this->expectExceptionMessage('Cannot locate radio with value: notexist'); + $c->selectByValue('notexist'); + } + + public function testSelectByIndex() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $c->selectByIndex(1); + + $allSelectedOptions = $c->getAllSelectedOptions(); + $this->assertCount(1, $allSelectedOptions); + $this->assertSame('j3b', $allSelectedOptions[0]->getAttribute('value')); + } + + public function testSelectByIndexInvalid() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(NoSuchElementException::class); + $this->expectExceptionMessage('Cannot locate radio with index: ' . PHP_INT_MAX); + $c->selectByIndex(PHP_INT_MAX); + } + + /** + * @dataProvider selectByVisibleTextDataProvider + * + * @param string $text + * @param string $value + */ + public function testSelectByVisibleText($text, $value) + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $c->selectByVisibleText($text); + $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + } + + /** + * @return array + */ + public function selectByVisibleTextDataProvider() + { + return [ + ['J 3 B', 'j3b'], + ['J3C', 'j3c'], + ]; + } + + /** + * @dataProvider selectByVisiblePartialTextDataProvider + * + * @param string $text + * @param string $value + */ + public function testSelectByVisiblePartialText($text, $value) + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $c->selectByVisiblePartialText($text); + $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + } + + /** + * @return array + */ + public function selectByVisiblePartialTextDataProvider() + { + return [ + ['3 B', 'j3b'], + ['3C', 'j3c'], + ]; + } + + public function testDeselectAllRadio() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage('You cannot deselect radio buttons'); + $c->deselectAll(); + } + + public function testDeselectByIndexRadio() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage('You cannot deselect radio buttons'); + $c->deselectByIndex(0); + } + + public function testDeselectByValueRadio() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage('You cannot deselect radio buttons'); + $c->deselectByValue('val'); + } + + public function testDeselectByVisibleTextRadio() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage('You cannot deselect radio buttons'); + $c->deselectByVisibleText('AB'); + } + + public function testDeselectByVisiblePartialTextRadio() + { + $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage('You cannot deselect radio buttons'); + $c->deselectByVisiblePartialText('AB'); + } +} diff --git a/tests/functional/web/form_checkbox_radio.html b/tests/functional/web/form_checkbox_radio.html new file mode 100644 index 000000000..b51dcfe47 --- /dev/null +++ b/tests/functional/web/form_checkbox_radio.html @@ -0,0 +1,43 @@ + + + + + Form + + +
+ + + + + + + + + + +
+ + + + + + + +
+ +
+ + From ce44465b74b1707a7e76b4f70f7d2fee6014ce94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 29 May 2018 15:17:26 +0200 Subject: [PATCH 004/373] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3088da9..95a4c6c73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased - ---- +### Added +- `WebDriverCheckboxes` and `WebDriverRadios` helper classes to simplify interaction with checkboxes and radio buttons. ## 1.6.0 - 2018-05-16 ### Added From 37c7b04a453d88d79e41e8ac4425603a04d17fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 16 Jun 2018 14:26:38 +0200 Subject: [PATCH 005/373] Fix the name of a env var in CONTRIBUTING.md (#583) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7b93736c..1a01c7adb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ test suite: ./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` environment variable: +simply set the `BROWSER_NAME` environment variable: ... export BROWSER_NAME="firefox" From be2985897851adca3e941d8c07c28512e24ee4a1 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Thu, 18 Oct 2018 13:53:38 -0700 Subject: [PATCH 006/373] Renamed license file. --- LICENCE.md => LICENSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENCE.md => LICENSE.md (100%) diff --git a/LICENCE.md b/LICENSE.md similarity index 100% rename from LICENCE.md rename to LICENSE.md From e020ae2811098d55cce705c66c52f1a4fbf2f9df Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Thu, 18 Oct 2018 13:58:23 -0700 Subject: [PATCH 007/373] Added copyright headers to 2 codefiles --- example.php | 14 ++++++++++++++ tests/bootstrap.php | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/example.php b/example.php index 2a9d0b298..f1109ed54 100644 --- a/example.php +++ b/example.php @@ -1,4 +1,18 @@ Date: Thu, 18 Oct 2018 14:14:22 -0700 Subject: [PATCH 008/373] Added a few more copyright headers. --- lib/Exception/IndexOutOfBoundsException.php | 13 +++++++++++++ lib/Exception/NoCollectionException.php | 13 +++++++++++++ lib/Exception/NoStringException.php | 13 +++++++++++++ lib/WebDriverSelectInterface.php | 13 +++++++++++++ tests/functional/web/slow_pixel.png.php | 13 +++++++++++++ tests/functional/web/submit.php | 16 +++++++++++++++- tests/functional/web/upload.php | 16 +++++++++++++++- 7 files changed, 95 insertions(+), 2 deletions(-) diff --git a/lib/Exception/IndexOutOfBoundsException.php b/lib/Exception/IndexOutOfBoundsException.php index ad52d21e9..de166b61e 100644 --- a/lib/Exception/IndexOutOfBoundsException.php +++ b/lib/Exception/IndexOutOfBoundsException.php @@ -1,4 +1,17 @@ + diff --git a/tests/functional/web/upload.php b/tests/functional/web/upload.php index 91a61bbcf..0bac36c3a 100644 --- a/tests/functional/web/upload.php +++ b/tests/functional/web/upload.php @@ -1,4 +1,18 @@ - + From 799fc83fb9bb0f61bfcb725db336a955774c22c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Thu, 6 Jun 2019 18:10:13 +0200 Subject: [PATCH 009/373] Improve test stability on SauceLabs; simplify doubleClick test to make it temporarily, as this action is not part of W3C WebDriver --- .travis.yml | 2 +- tests/functional/RemoteWebDriverTest.php | 26 +++++++++++++++------- tests/functional/WebDriverActionsTest.php | 5 +---- tests/functional/WebDriverTestCase.php | 4 ++-- tests/functional/WebDriverTimeoutsTest.php | 4 ++-- tests/functional/web/delayed_element.html | 2 +- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4a2cada83..a268f0b4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,7 +58,7 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - php: 7.2 - env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="15.15063" PLATFORM="Windows 10" + env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="16.16299" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index 818a9da5a..d996d304a 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -145,6 +145,7 @@ public function testShouldCloseWindow() /** * @covers ::executeScript + * @group exclude-saucelabs */ public function testShouldExecuteScriptAndDoNotBlockExecution() { @@ -153,17 +154,18 @@ public function testShouldExecuteScriptAndDoNotBlockExecution() $element = $this->driver->findElement(WebDriverBy::id('id_test')); $this->assertSame('Test by ID', $element->getText()); + $start = microtime(true); $this->driver->executeScript(' setTimeout( - function(){document.getElementById("id_test").innerHTML = "Text changed by script"}, - 500 + function(){document.getElementById("id_test").innerHTML = "Text changed by script";}, + 250 )'); + $end = microtime(true); - // Make sure the script don't block the test execution - $this->assertSame('Test by ID', $element->getText()); + $this->assertLessThan(250, $end - $start, 'executeScript() should not block execution'); - // If we wait, the script should be executed - usleep(1000000); // wait 1000 ms + // If we wait, the script should be executed and its value changed + usleep(300000); // wait 300 ms $this->assertSame('Text changed by script', $element->getText()); } @@ -180,6 +182,7 @@ public function testShouldExecuteAsyncScriptAndWaitUntilItIsFinished() $element = $this->driver->findElement(WebDriverBy::id('id_test')); $this->assertSame('Test by ID', $element->getText()); + $start = microtime(true); $this->driver->executeAsyncScript( 'var callback = arguments[arguments.length - 1]; setTimeout( @@ -190,6 +193,13 @@ function(){ 250 );' ); + $end = microtime(true); + + $this->assertGreaterThan( + 0.250, + $end - $start, + 'executeAsyncScript() should block execution until callback() is called' + ); // The result must be immediately available, as the executeAsyncScript should block the execution until the // callback is called. @@ -204,7 +214,7 @@ public function testShouldTakeScreenshot() if (!extension_loaded('gd')) { $this->markTestSkipped('GD extension must be enabled'); } - if ($this->desiredCapabilities->getBrowserName() == WebDriverBrowserType::HTMLUNIT) { + if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { $this->markTestSkipped('Screenshots are not supported by HtmlUnit browser'); } @@ -227,7 +237,7 @@ public function testShouldSaveScreenshotToFile() if (!extension_loaded('gd')) { $this->markTestSkipped('GD extension must be enabled'); } - if ($this->desiredCapabilities->getBrowserName() == WebDriverBrowserType::HTMLUNIT) { + if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { $this->markTestSkipped('Screenshots are not supported by HtmlUnit browser'); } diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 188c76cd9..3df984810 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -122,10 +122,7 @@ public function testShouldDoubleClickOnElement() ->doubleClick($element) ->perform(); - $this->assertSame( - ['mouseover item-3', 'mousedown item-3', 'mouseup item-3', 'click item-3', 'dblclick item-3'], - $this->retrieveLoggedEvents() - ); + $this->assertContains('dblclick item-3', $this->retrieveLoggedEvents()); } /** diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index e9714a87c..5fe6a7e8a 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -44,7 +44,7 @@ protected function setUp() { $this->desiredCapabilities = new DesiredCapabilities(); - if ($this->isSauceLabsBuild()) { + if (static::isSauceLabsBuild()) { $this->setUpSauceLabs(); } else { if (getenv('BROWSER_NAME')) { @@ -87,7 +87,7 @@ protected function tearDown() /** * @return bool */ - public function isSauceLabsBuild() + public static function isSauceLabsBuild() { return getenv('SAUCELABS') ? true : false; } diff --git a/tests/functional/WebDriverTimeoutsTest.php b/tests/functional/WebDriverTimeoutsTest.php index 69d143cc4..733be545c 100644 --- a/tests/functional/WebDriverTimeoutsTest.php +++ b/tests/functional/WebDriverTimeoutsTest.php @@ -42,7 +42,7 @@ public function testShouldGetDelayedElementWithImplicitWait() { $this->driver->get($this->getTestPageUrl('delayed_element.html')); - $this->driver->manage()->timeouts()->implicitlyWait(1); + $this->driver->manage()->timeouts()->implicitlyWait(2); $element = $this->driver->findElement(WebDriverBy::id('delayed')); $this->assertInstanceOf(RemoteWebElement::class, $element); @@ -54,7 +54,7 @@ public function testShouldGetDelayedElementWithImplicitWait() */ public function testShouldFailIfPageIsLoadingLongerThanPageLoadTimeout() { - if ($this->desiredCapabilities->getBrowserName() == WebDriverBrowserType::HTMLUNIT) { + if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { $this->markTestSkipped('Not supported by HtmlUnit browser'); } diff --git a/tests/functional/web/delayed_element.html b/tests/functional/web/delayed_element.html index b0f057a9a..4572cd342 100644 --- a/tests/functional/web/delayed_element.html +++ b/tests/functional/web/delayed_element.html @@ -12,7 +12,7 @@ setTimeout(function () { var wrapper = document.getElementById("wrapper"); wrapper.innerHTML = '
Element appearing after 500ms
'; - }, 500); + }, 1500); From 6d488eacd23db77822a748725a1c9dafa6d0221e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 8 Jun 2019 12:33:18 +0200 Subject: [PATCH 010/373] Force Chrome 74 on saucelabs, as 75 uses W3C protocol by default --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a268f0b4d..c97b22525 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - php: 7.2 - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="latest" PLATFORM="Windows 10" + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" From 5f7a311bde888283615e8b3767a92302c5e29515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 7 Jun 2019 17:29:22 +0200 Subject: [PATCH 011/373] Use PHP 7.3 as the main PHP version for travis builds --- .travis.yml | 55 +++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index c97b22525..082a6efb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,11 @@ sudo: false dist: trusty php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 + - '5.6' + - '7.0' + - '7.1' + - '7.2' + - '7.3' env: global: @@ -16,30 +17,43 @@ env: matrix: include: + # Codestyle check build + - php: '7.3' + env: CHECK_CODESTYLE=1 + before_install: + - phpenv config-rm xdebug.ini + before_script: ~ + script: + - composer require phpstan/phpstan-shim # Not part of require-dev, because it won't install on PHP 5.6 + - composer analyze + - composer codestyle:check + after_script: ~ + after_success: ~ + + # Build with lowest possible dependencies on lowest possible PHP + - php: '5.6' + env: DEPENDENCIES="--prefer-lowest" + # Add build to run tests against Firefox inside Travis environment - - php: 7.2 + - php: '7.3' env: BROWSER_NAME="firefox" addons: firefox: "45.8.0esr" # Add build to run tests against Chrome inside Travis environment - - php: 7.2 + - php: '7.3' env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" addons: chrome: stable - # Build with lowest possible dependencies - - php: 7.2 - env: DEPENDENCIES="--prefer-lowest" - # Chrome on Travis build with lowest possible dependencies - - php: 7.2 + - php: '7.3' env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" DEPENDENCIES="--prefer-lowest" addons: chrome: stable # Saucelabs builds - - php: 7.2 + - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & @@ -48,7 +62,7 @@ matrix: sauce_connect: true jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: 7.2 + - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & @@ -57,7 +71,7 @@ matrix: sauce_connect: true jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: 7.2 + - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="16.16299" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & @@ -67,19 +81,6 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - # Codestyle check build - - php: 7.2 - env: CHECK_CODESTYLE=1 - before_install: - - phpenv config-rm xdebug.ini - before_script: ~ - script: - - composer require phpstan/phpstan-shim # Not part of require-dev, because it won't install on PHP 5.6 - - composer analyze - - composer codestyle:check - after_script: ~ - after_success: ~ - cache: directories: - $HOME/.composer/cache From 242fffeb4caf279a87f75a01b29a87e66a3e1a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 8 Jun 2019 15:02:51 +0200 Subject: [PATCH 012/373] Disable WebDriverSelectTest on saucelabs - it is too slow and has minimal value --- tests/functional/WebDriverCheckboxesTest.php | 4 ++++ tests/functional/WebDriverRadiosTest.php | 4 ++++ tests/functional/WebDriverSelectTest.php | 1 + 3 files changed, 9 insertions(+) diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index 3bf8ccffd..128d298d5 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -17,6 +17,10 @@ use Facebook\WebDriver\Exception\NoSuchElementException; +/** + * @covers \Facebook\WebDriver\WebDriverCheckboxes + * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + */ class WebDriverCheckboxesTest extends WebDriverTestCase { protected function setUp() diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index f28417ac4..2a1a9869d 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -18,6 +18,10 @@ use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Exception\UnsupportedOperationException; +/** + * @covers \Facebook\WebDriver\WebDriverRadios + * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + */ class WebDriverRadiosTest extends WebDriverTestCase { protected function setUp() diff --git a/tests/functional/WebDriverSelectTest.php b/tests/functional/WebDriverSelectTest.php index 37ad416af..b25dd03af 100644 --- a/tests/functional/WebDriverSelectTest.php +++ b/tests/functional/WebDriverSelectTest.php @@ -20,6 +20,7 @@ use Facebook\WebDriver\Exception\UnsupportedOperationException; /** + * @group exclude-saucelabs * @covers \Facebook\WebDriver\WebDriverSelect * @covers \Facebook\WebDriver\Exception\UnexpectedTagNameException */ From 9be6cbbd8006a24fb180df75cb4b9796a2aad770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 8 Jun 2019 15:42:49 +0200 Subject: [PATCH 013/373] Remove mostly redundant second lowest-deps build --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 082a6efb8..b3ee784e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,12 +46,6 @@ matrix: addons: chrome: stable - # Chrome on Travis build with lowest possible dependencies - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" DEPENDENCIES="--prefer-lowest" - addons: - chrome: stable - # Saucelabs builds - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" From 0f3933c41606fd076d79ff064bc0def2b774e67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 8 Jun 2019 22:50:56 +0200 Subject: [PATCH 014/373] Disable default W3C protocol in Chrome 75+ --- .travis.yml | 27 ++++++++++++++++++++++----- lib/Remote/HttpCommandExecutor.php | 5 +++++ lib/Remote/RemoteWebDriver.php | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b3ee784e8..b6850baf1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ env: global: - DISPLAY=:99.0 - BROWSER_NAME="htmlunit" - - CHROMEDRIVER_VERSION="2.38" matrix: include: @@ -34,15 +33,21 @@ matrix: - php: '5.6' env: DEPENDENCIES="--prefer-lowest" - # Add build to run tests against Firefox inside Travis environment + # Firefox inside Travis environment - php: '7.3' env: BROWSER_NAME="firefox" addons: firefox: "45.8.0esr" - # Add build to run tests against Chrome inside Travis environment + # Stable Chrome + Chromedriver 74 inside Travis environment - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" + env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="74.0.3729.6" + addons: + chrome: stable + + # Stable Chrome + Chromedriver 75+ inside Travis environment + - php: '7.3' + env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.8" addons: chrome: stable @@ -56,8 +61,9 @@ matrix: sauce_connect: true jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= + - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" # 74 is the last version which don't use W3C WebDriver by default before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -65,6 +71,17 @@ matrix: sauce_connect: true jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= + + - php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="75.0" PLATFORM="Windows 10" + before_script: + - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & + - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" + addons: + sauce_connect: true + jwt: + secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= + - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="16.16299" PLATFORM="Windows 10" before_script: diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index f584324b7..1891fdb35 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -273,6 +273,11 @@ public function execute(WebDriverCommand $command) if ($http_method === 'POST' && $params && is_array($params)) { $encoded_params = json_encode($params); + } elseif ($http_method === 'POST' && $encoded_params === null) { + // Workaround for bug https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943 in Chrome 75. + // Chromedriver now erroneously does not allow POST body to be empty even for the JsonWire protocol. + // If the command POST is empty, here we send some dummy data as a workaround: + $encoded_params = json_encode(['_' => '_']); } curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 1487689ec..538ae282f 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -15,6 +15,7 @@ namespace Facebook\WebDriver\Remote; +use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Interactions\WebDriverActions; use Facebook\WebDriver\JavaScriptExecutor; use Facebook\WebDriver\WebDriver; @@ -102,6 +103,19 @@ public static function create( $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); + // Hotfix: W3C WebDriver protocol is not yet supported by php-webdriver, so we must force Chromedriver to + // not use the W3C protocol by default (which is what Chromedriver does starting with version 75). + if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME) { + $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY); + $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions(); + + if (!isset($chromeOptions->toArray()['w3c'])) { + $chromeOptions->setExperimentalOption('w3c', false); + } + + $desired_capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); + } + $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); From 550edf5a815694f60654a0a82d0bbe818eeb1514 Mon Sep 17 00:00:00 2001 From: JorisVanEijden Date: Mon, 25 Feb 2019 12:38:04 +0100 Subject: [PATCH 015/373] Do not send null values in cookie array (fixes #626) --- lib/Cookie.php | 50 ++++++++++++++++++++------------------- tests/unit/CookieTest.php | 22 +++++++++++++++++ 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/lib/Cookie.php b/lib/Cookie.php index 57ec7e13a..216ac3033 100644 --- a/lib/Cookie.php +++ b/lib/Cookie.php @@ -27,15 +27,7 @@ class Cookie implements \ArrayAccess { /** @var array */ - protected $cookie = [ - 'name' => null, - 'value' => null, - 'path' => null, - 'domain' => null, - 'expiry' => null, - 'secure' => null, - 'httpOnly' => null, - ]; + protected $cookie = []; /** * @param string $name The name of the cookie; may not be null or an empty string. @@ -51,11 +43,17 @@ public function __construct($name, $value) } /** - * @param array $cookieArray + * @param array $cookieArray The cookie fields; must contain name and value. * @return Cookie */ public static function createFromArray(array $cookieArray) { + if (!isset($cookieArray['name'])) { + throw new InvalidArgumentException('Cookie name should be set'); + } + if (!isset($cookieArray['value'])) { + throw new InvalidArgumentException('Cookie value should be set'); + } $cookie = new self($cookieArray['name'], $cookieArray['value']); if (isset($cookieArray['path'])) { @@ -82,7 +80,7 @@ public static function createFromArray(array $cookieArray) */ public function getName() { - return $this->cookie['name']; + return $this->offsetGet('name'); } /** @@ -90,7 +88,7 @@ public function getName() */ public function getValue() { - return $this->cookie['value']; + return $this->offsetGet('value'); } /** @@ -100,7 +98,7 @@ public function getValue() */ public function setPath($path) { - $this->cookie['path'] = $path; + $this->offsetSet('path', $path); } /** @@ -108,7 +106,7 @@ public function setPath($path) */ public function getPath() { - return $this->cookie['path']; + return $this->offsetGet('path'); } /** @@ -122,7 +120,7 @@ public function setDomain($domain) throw new InvalidArgumentException(sprintf('Cookie domain "%s" should not contain a port', $domain)); } - $this->cookie['domain'] = $domain; + $this->offsetSet('domain', $domain); } /** @@ -130,7 +128,7 @@ public function setDomain($domain) */ public function getDomain() { - return $this->cookie['domain']; + return $this->offsetGet('domain'); } /** @@ -140,7 +138,7 @@ public function getDomain() */ public function setExpiry($expiry) { - $this->cookie['expiry'] = (int) $expiry; + $this->offsetSet('expiry', (int) $expiry); } /** @@ -148,7 +146,7 @@ public function setExpiry($expiry) */ public function getExpiry() { - return $this->cookie['expiry']; + return $this->offsetGet('expiry'); } /** @@ -158,7 +156,7 @@ public function getExpiry() */ public function setSecure($secure) { - $this->cookie['secure'] = $secure; + $this->offsetSet('secure', $secure); } /** @@ -166,7 +164,7 @@ public function setSecure($secure) */ public function isSecure() { - return $this->cookie['secure']; + return $this->offsetGet('secure'); } /** @@ -176,7 +174,7 @@ public function isSecure() */ public function setHttpOnly($httpOnly) { - $this->cookie['httpOnly'] = $httpOnly; + $this->offsetSet('httpOnly', $httpOnly); } /** @@ -184,7 +182,7 @@ public function setHttpOnly($httpOnly) */ public function isHttpOnly() { - return $this->cookie['httpOnly']; + return $this->offsetGet('httpOnly'); } /** @@ -202,12 +200,16 @@ public function offsetExists($offset) public function offsetGet($offset) { - return $this->cookie[$offset]; + return $this->offsetExists($offset) ? $this->cookie[$offset] : null; } public function offsetSet($offset, $value) { - $this->cookie[$offset] = $value; + if ($value === null) { + unset($this->cookie[$offset]); + } else { + $this->cookie[$offset] = $value; + } } public function offsetUnset($offset) diff --git a/tests/unit/CookieTest.php b/tests/unit/CookieTest.php index 3a0679d2c..5fdb43038 100644 --- a/tests/unit/CookieTest.php +++ b/tests/unit/CookieTest.php @@ -62,6 +62,28 @@ public function testShouldBeConvertibleToArray(Cookie $cookie) ); } + /** + * Test that there are no null values in the cookie array. + * + * Both JsonWireProtocol and w3c protocol say to leave an entry off + * rather than having a null value. + * + * https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol + * https://w3c.github.io/webdriver/#add-cookie + */ + public function testShouldNotContainNullValues() + { + $cookie = new Cookie('cookieName', 'someValue'); + + $cookie->setHttpOnly(null); + $cookie->setPath(null); + $cookieArray = $cookie->toArray(); + + foreach ($cookieArray as $key => $value) { + $this->assertNotNull($value, $key . ' should not be null'); + } + } + /** * @depends testShouldSetAllProperties * @param Cookie $cookie From d1ad0b14b4f34f3d3eca10c1587b991f612e6746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 10 Jun 2019 13:28:48 +0200 Subject: [PATCH 016/373] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a4c6c73..e27bd1b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ### Added - `WebDriverCheckboxes` and `WebDriverRadios` helper classes to simplify interaction with checkboxes and radio buttons. +### Fixed +- Stop sending null values in Cookie object, which is against the protocol and may cause request to remote ends to fail. + +### Changed +- Force Chrome to not use W3C WebDriver protocol. +- Add workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943) which breaks the protocol in Chromedriver 75. + ## 1.6.0 - 2018-05-16 ### Added - Connection and request timeouts could be specified also when creating RemoteWebDriver from existing session ID. From 12ac107aba8af2cc08c549884f5a5844899509e9 Mon Sep 17 00:00:00 2001 From: Oleg Andreyev Date: Mon, 31 Dec 2018 02:23:12 +0200 Subject: [PATCH 017/373] Improved xpath matching related elements to handle input associated to different
Added @form attribute check, because it's possible to put into a form which is not related to it, see MDN https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form --- lib/AbstractWebDriverCheckboxOrRadio.php | 17 +++++++++++------ tests/functional/WebDriverCheckboxesTest.php | 6 ++++++ tests/functional/WebDriverRadiosTest.php | 6 ++++++ tests/functional/web/form_checkbox_radio.html | 14 +++++++++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/AbstractWebDriverCheckboxOrRadio.php b/lib/AbstractWebDriverCheckboxOrRadio.php index 46288592b..f70a2ef04 100644 --- a/lib/AbstractWebDriverCheckboxOrRadio.php +++ b/lib/AbstractWebDriverCheckboxOrRadio.php @@ -218,12 +218,17 @@ protected function getRelatedElements($value = null) } } - return $this->element->findElements(WebDriverBy::xpath(sprintf( - '//form[@id = %1$s]//input[@name = %2$s%3$s] | //input[@form = %1$s and @name = %2$s%3$s]', - XPathEscaper::escapeQuotes($formId), - XPathEscaper::escapeQuotes($this->name), - $valueSelector - ))); + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form + return $this->element->findElements( + WebDriverBy::xpath(sprintf( + '//form[@id = %1$s]//input[@name = %2$s%3$s' + . ' and ((boolean(@form) = true() and @form = %1$s) or boolean(@form) = false())]' + . ' | //input[@form = %1$s and @name = %2$s%3$s]', + XPathEscaper::escapeQuotes($formId), + XPathEscaper::escapeQuotes($this->name), + $valueSelector + )) + ); } /** diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index 128d298d5..f7e1407b6 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -51,6 +51,12 @@ public function testGetFirstSelectedOption() $this->assertSame('j2a', $c->getFirstSelectedOption()->getAttribute('value')); } + public function testGetFirstSelectedOptionWithSameNameDifferentForm() + { + $radio = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@id="j5b"]'))); + $this->assertEquals('j5b', $radio->getFirstSelectedOption()->getAttribute('value')); + } + public function testSelectByValue() { $selectedOptions = ['j2b', 'j2c']; diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index 2a1a9869d..369717eb2 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -55,6 +55,12 @@ public function testGetFirstSelectedOption() $this->assertSame('j3a', $c->getFirstSelectedOption()->getAttribute('value')); } + public function testGetFirstSelectedOptionWithSameNameDifferentForm() + { + $radio = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@id="j4b"]'))); + $this->assertEquals('j4b', $radio->getFirstSelectedOption()->getAttribute('value')); + } + public function testSelectByValue() { $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); diff --git a/tests/functional/web/form_checkbox_radio.html b/tests/functional/web/form_checkbox_radio.html index b51dcfe47..1e13d74e4 100644 --- a/tests/functional/web/form_checkbox_radio.html +++ b/tests/functional/web/form_checkbox_radio.html @@ -27,6 +27,12 @@ + + + + + +
@@ -36,7 +42,13 @@ -
+ + + + + + +
From 87f9d0f4dbaf8ecf7c87415cccd3f00ea9e0311c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 10 Jun 2019 13:06:00 +0200 Subject: [PATCH 018/373] Improve readability of tests by structuring theme more in arrange-act-assert way --- tests/functional/WebDriverCheckboxesTest.php | 139 +++++++++++------- tests/functional/WebDriverRadiosTest.php | 73 ++++----- tests/functional/web/form_checkbox_radio.html | 18 ++- 3 files changed, 137 insertions(+), 93 deletions(-) diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index f7e1407b6..a37fde342 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -32,42 +32,55 @@ protected function setUp() public function testIsMultiple() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); - $this->assertTrue($c->isMultiple()); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); + + $this->assertTrue($checkboxes->isMultiple()); } public function testGetOptions() { - $c = new WebDriverCheckboxes( + $checkboxes = new WebDriverCheckboxes( $this->driver->findElement(WebDriverBy::xpath('//form[2]//input[@type="checkbox"]')) ); - $this->assertNotEmpty($c->getOptions()); + + $this->assertNotEmpty($checkboxes->getOptions()); } public function testGetFirstSelectedOption() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); - $c->selectByValue('j2a'); - $this->assertSame('j2a', $c->getFirstSelectedOption()->getAttribute('value')); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); + + $checkboxes->selectByValue('j2a'); + + $this->assertSame('j2a', $checkboxes->getFirstSelectedOption()->getAttribute('value')); } - public function testGetFirstSelectedOptionWithSameNameDifferentForm() + public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm() { - $radio = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@id="j5b"]'))); - $this->assertEquals('j5b', $radio->getFirstSelectedOption()->getAttribute('value')); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@id="j5b"]')) + ); + + $this->assertEquals('j5b', $checkboxes->getFirstSelectedOption()->getAttribute('value')); } public function testSelectByValue() { $selectedOptions = ['j2b', 'j2c']; - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); foreach ($selectedOptions as $index => $selectedOption) { - $c->selectByValue($selectedOption); + $checkboxes->selectByValue($selectedOption); } $selectedValues = []; - foreach ($c->getAllSelectedOptions() as $option) { + foreach ($checkboxes->getAllSelectedOptions() as $option) { $selectedValues[] = $option->getAttribute('value'); } $this->assertSame($selectedOptions, $selectedValues); @@ -75,24 +88,28 @@ public function testSelectByValue() public function testSelectByValueInvalid() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); $this->expectException(NoSuchElementException::class); $this->expectExceptionMessage('Cannot locate checkbox with value: notexist'); - $c->selectByValue('notexist'); + $checkboxes->selectByValue('notexist'); } public function testSelectByIndex() { $selectedOptions = [1 => 'j2b', 2 => 'j2c']; - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); foreach ($selectedOptions as $index => $selectedOption) { - $c->selectByIndex($index); + $checkboxes->selectByIndex($index); } $selectedValues = []; - foreach ($c->getAllSelectedOptions() as $option) { + foreach ($checkboxes->getAllSelectedOptions() as $option) { $selectedValues[] = $option->getAttribute('value'); } $this->assertSame(array_values($selectedOptions), $selectedValues); @@ -100,11 +117,13 @@ public function testSelectByIndex() public function testSelectByIndexInvalid() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); $this->expectException(NoSuchElementException::class); $this->expectExceptionMessage('Cannot locate checkbox with index: ' . PHP_INT_MAX); - $c->selectByIndex(PHP_INT_MAX); + $checkboxes->selectByIndex(PHP_INT_MAX); } /** @@ -115,9 +134,13 @@ public function testSelectByIndexInvalid() */ public function testSelectByVisibleText($text, $value) { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); - $c->selectByVisibleText($text); - $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); + + $checkboxes->selectByVisibleText($text); + + $this->assertSame($value, $checkboxes->getFirstSelectedOption()->getAttribute('value')); } /** @@ -139,9 +162,13 @@ public function selectByVisibleTextDataProvider() */ public function testSelectByVisiblePartialText($text, $value) { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); - $c->selectByVisiblePartialText($text); - $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); + + $checkboxes->selectByVisiblePartialText($text); + + $this->assertSame($value, $checkboxes->getFirstSelectedOption()->getAttribute('value')); } /** @@ -157,51 +184,61 @@ public function selectByVisiblePartialTextDataProvider() public function testDeselectAll() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); - $c->selectByIndex(0); - $this->assertCount(1, $c->getAllSelectedOptions()); - $c->deselectAll(); - $this->assertEmpty($c->getAllSelectedOptions()); + $checkboxes->selectByIndex(0); + $this->assertCount(1, $checkboxes->getAllSelectedOptions()); + $checkboxes->deselectAll(); + $this->assertEmpty($checkboxes->getAllSelectedOptions()); } public function testDeselectByIndex() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); - $c->selectByIndex(0); - $this->assertCount(1, $c->getAllSelectedOptions()); - $c->deselectByIndex(0); - $this->assertEmpty($c->getAllSelectedOptions()); + $checkboxes->selectByIndex(0); + $this->assertCount(1, $checkboxes->getAllSelectedOptions()); + $checkboxes->deselectByIndex(0); + $this->assertEmpty($checkboxes->getAllSelectedOptions()); } public function testDeselectByValue() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); - $c->selectByValue('j2a'); - $this->assertCount(1, $c->getAllSelectedOptions()); - $c->deselectByValue('j2a'); - $this->assertEmpty($c->getAllSelectedOptions()); + $checkboxes->selectByValue('j2a'); + $this->assertCount(1, $checkboxes->getAllSelectedOptions()); + $checkboxes->deselectByValue('j2a'); + $this->assertEmpty($checkboxes->getAllSelectedOptions()); } public function testDeselectByVisibleText() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); - $c->selectByVisibleText('J 2 B'); - $this->assertCount(1, $c->getAllSelectedOptions()); - $c->deselectByVisibleText('J 2 B'); - $this->assertEmpty($c->getAllSelectedOptions()); + $checkboxes->selectByVisibleText('J 2 B'); + $this->assertCount(1, $checkboxes->getAllSelectedOptions()); + $checkboxes->deselectByVisibleText('J 2 B'); + $this->assertEmpty($checkboxes->getAllSelectedOptions()); } public function testDeselectByVisiblePartialText() { - $c = new WebDriverCheckboxes($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]')) + ); - $c->selectByVisiblePartialText('2C'); - $this->assertCount(1, $c->getAllSelectedOptions()); - $c->deselectByVisiblePartialText('2C'); - $this->assertEmpty($c->getAllSelectedOptions()); + $checkboxes->selectByVisiblePartialText('2C'); + $this->assertCount(1, $checkboxes->getAllSelectedOptions()); + $checkboxes->deselectByVisiblePartialText('2C'); + $this->assertEmpty($checkboxes->getAllSelectedOptions()); } } diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index 369717eb2..2350a401d 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -33,15 +33,16 @@ protected function setUp() public function testIsMultiple() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); - $this->assertFalse($c->isMultiple()); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $this->assertFalse($radios->isMultiple()); } public function testGetOptions() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $values = []; - foreach ($c->getOptions() as $option) { + foreach ($radios->getOptions() as $option) { $values[] = $option->getAttribute('value'); } @@ -50,53 +51,57 @@ public function testGetOptions() public function testGetFirstSelectedOption() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); - $c->selectByValue('j3a'); - $this->assertSame('j3a', $c->getFirstSelectedOption()->getAttribute('value')); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + + $radios->selectByValue('j3a'); + + $this->assertSame('j3a', $radios->getFirstSelectedOption()->getAttribute('value')); } - public function testGetFirstSelectedOptionWithSameNameDifferentForm() + public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm() { $radio = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@id="j4b"]'))); + $this->assertEquals('j4b', $radio->getFirstSelectedOption()->getAttribute('value')); } public function testSelectByValue() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); - $c->selectByValue('j3b'); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios->selectByValue('j3b'); + + $selectedOptions = $radios->getAllSelectedOptions(); - $selectedOptions = $c->getAllSelectedOptions(); $this->assertCount(1, $selectedOptions); $this->assertSame('j3b', $selectedOptions[0]->getAttribute('value')); } public function testSelectByValueInvalid() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(NoSuchElementException::class); $this->expectExceptionMessage('Cannot locate radio with value: notexist'); - $c->selectByValue('notexist'); + $radios->selectByValue('notexist'); } public function testSelectByIndex() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); - $c->selectByIndex(1); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios->selectByIndex(1); - $allSelectedOptions = $c->getAllSelectedOptions(); + $allSelectedOptions = $radios->getAllSelectedOptions(); $this->assertCount(1, $allSelectedOptions); $this->assertSame('j3b', $allSelectedOptions[0]->getAttribute('value')); } public function testSelectByIndexInvalid() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(NoSuchElementException::class); $this->expectExceptionMessage('Cannot locate radio with index: ' . PHP_INT_MAX); - $c->selectByIndex(PHP_INT_MAX); + $radios->selectByIndex(PHP_INT_MAX); } /** @@ -107,9 +112,9 @@ public function testSelectByIndexInvalid() */ public function testSelectByVisibleText($text, $value) { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); - $c->selectByVisibleText($text); - $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios->selectByVisibleText($text); + $this->assertSame($value, $radios->getFirstSelectedOption()->getAttribute('value')); } /** @@ -131,9 +136,9 @@ public function selectByVisibleTextDataProvider() */ public function testSelectByVisiblePartialText($text, $value) { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); - $c->selectByVisiblePartialText($text); - $this->assertSame($value, $c->getFirstSelectedOption()->getAttribute('value')); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios->selectByVisiblePartialText($text); + $this->assertSame($value, $radios->getFirstSelectedOption()->getAttribute('value')); } /** @@ -149,46 +154,46 @@ public function selectByVisiblePartialTextDataProvider() public function testDeselectAllRadio() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(UnsupportedOperationException::class); $this->expectExceptionMessage('You cannot deselect radio buttons'); - $c->deselectAll(); + $radios->deselectAll(); } public function testDeselectByIndexRadio() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(UnsupportedOperationException::class); $this->expectExceptionMessage('You cannot deselect radio buttons'); - $c->deselectByIndex(0); + $radios->deselectByIndex(0); } public function testDeselectByValueRadio() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(UnsupportedOperationException::class); $this->expectExceptionMessage('You cannot deselect radio buttons'); - $c->deselectByValue('val'); + $radios->deselectByValue('val'); } public function testDeselectByVisibleTextRadio() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(UnsupportedOperationException::class); $this->expectExceptionMessage('You cannot deselect radio buttons'); - $c->deselectByVisibleText('AB'); + $radios->deselectByVisibleText('AB'); } public function testDeselectByVisiblePartialTextRadio() { - $c = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); $this->expectException(UnsupportedOperationException::class); $this->expectExceptionMessage('You cannot deselect radio buttons'); - $c->deselectByVisiblePartialText('AB'); + $radios->deselectByVisiblePartialText('AB'); } } diff --git a/tests/functional/web/form_checkbox_radio.html b/tests/functional/web/form_checkbox_radio.html index 1e13d74e4..3e7c2a19a 100644 --- a/tests/functional/web/form_checkbox_radio.html +++ b/tests/functional/web/form_checkbox_radio.html @@ -5,7 +5,7 @@ Form -
+ @@ -27,28 +27,30 @@ + - + - +
+ - + - + - + - + -
+
From e8accbdea92c617e0bfea7bcd5aea522e0c19c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 10 Jun 2019 13:20:45 +0200 Subject: [PATCH 019/373] Extend testcases to cover also form without id --- tests/functional/WebDriverCheckboxesTest.php | 9 +++++++++ tests/functional/WebDriverRadiosTest.php | 13 +++++++++++-- tests/functional/web/form_checkbox_radio.html | 12 ++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index a37fde342..48b72a53c 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -68,6 +68,15 @@ public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociate $this->assertEquals('j5b', $checkboxes->getFirstSelectedOption()->getAttribute('value')); } + public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentFormWithoutId() + { + $checkboxes = new WebDriverCheckboxes( + $this->driver->findElement(WebDriverBy::xpath('//input[@id="j5d"]')) + ); + + $this->assertEquals('j5c', $checkboxes->getFirstSelectedOption()->getAttribute('value')); + } + public function testSelectByValue() { $selectedOptions = ['j2b', 'j2c']; diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index 2350a401d..4d659772b 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -60,9 +60,18 @@ public function testGetFirstSelectedOption() public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentForm() { - $radio = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@id="j4b"]'))); + $radios = new WebDriverRadios($this->driver->findElement(WebDriverBy::xpath('//input[@id="j4b"]'))); - $this->assertEquals('j4b', $radio->getFirstSelectedOption()->getAttribute('value')); + $this->assertEquals('j4b', $radios->getFirstSelectedOption()->getAttribute('value')); + } + + public function testShouldGetFirstSelectedOptionConsideringOnlyElementsAssociatedWithCurrentFormWithoutId() + { + $radios = new WebDriverRadios( + $this->driver->findElement(WebDriverBy::xpath('//input[@id="j4c"]')) + ); + + $this->assertEquals('j4c', $radios->getFirstSelectedOption()->getAttribute('value')); } public function testSelectByValue() diff --git a/tests/functional/web/form_checkbox_radio.html b/tests/functional/web/form_checkbox_radio.html index 3e7c2a19a..fe59896d9 100644 --- a/tests/functional/web/form_checkbox_radio.html +++ b/tests/functional/web/form_checkbox_radio.html @@ -53,5 +53,17 @@
+ + +
+ + + + + + + +
+ From 9a52b1ac036743c31bc127fd7f2f4710e2bc968a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 10 Jun 2019 16:32:25 +0200 Subject: [PATCH 020/373] Release version 1.7.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e27bd1b89..9b5487916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased + +## 1.7.0 - 2019-06-10 ### Added - `WebDriverCheckboxes` and `WebDriverRadios` helper classes to simplify interaction with checkboxes and radio buttons. From ec7ce5b611e3a73eb789c783760487c89b4999a1 Mon Sep 17 00:00:00 2001 From: Lctrs Date: Tue, 11 Jun 2019 11:47:33 +0200 Subject: [PATCH 021/373] Do not fail if ChromeOptions is already an array DesiredCapabilities::toArray() is a mutable operation which can lead to undesirable side effect of converting ChromeOptions to an array. --- lib/Remote/DesiredCapabilities.php | 1 + lib/Remote/RemoteWebDriver.php | 4 +++- tests/functional/RemoteWebDriverCreateTest.php | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/Remote/DesiredCapabilities.php b/lib/Remote/DesiredCapabilities.php index 0e1a1ab25..533c8873a 100644 --- a/lib/Remote/DesiredCapabilities.php +++ b/lib/Remote/DesiredCapabilities.php @@ -156,6 +156,7 @@ public function setJavascriptEnabled($enabled) } /** + * @todo Remove side-effects - not change ie. ChromeOptions::CAPABILITY from instance of ChromeOptions to an array * @return array */ public function toArray() diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 538ae282f..af729f16e 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -109,8 +109,10 @@ public static function create( $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY); $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions(); - if (!isset($chromeOptions->toArray()['w3c'])) { + if ($chromeOptions instanceof ChromeOptions && !isset($chromeOptions->toArray()['w3c'])) { $chromeOptions->setExperimentalOption('w3c', false); + } elseif (is_array($chromeOptions) && !isset($chromeOptions['w3c'])) { + $chromeOptions['w3c'] = false; } $desired_capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); diff --git a/tests/functional/RemoteWebDriverCreateTest.php b/tests/functional/RemoteWebDriverCreateTest.php index f9d7e809e..90c072922 100644 --- a/tests/functional/RemoteWebDriverCreateTest.php +++ b/tests/functional/RemoteWebDriverCreateTest.php @@ -49,6 +49,19 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver() $this->assertSame($this->desiredCapabilities->getBrowserName(), $returnedCapabilities->getBrowserName()); } + public function testShouldAcceprCapabilitiesAsAnArray() + { + // Method has a side-effect of converting whole content of desiredCapabilities to an array + $this->desiredCapabilities->toArray(); + + $this->driver = RemoteWebDriver::create( + $this->serverUrl, + $this->desiredCapabilities, + $this->connectionTimeout, + $this->requestTimeout + ); + } + public function testShouldCreateWebDriverWithRequiredCapabilities() { $requiredCapabilities = new DesiredCapabilities(); From f650ee73645a3e95fb0567b25d4f33c13664cbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Wed, 12 Jun 2019 11:32:08 +0200 Subject: [PATCH 022/373] Do not send w3c capability as a workaround for browsestack schema validation (fixes #644) --- lib/Remote/RemoteWebDriver.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index af729f16e..71f45704e 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -105,7 +105,9 @@ public static function create( // Hotfix: W3C WebDriver protocol is not yet supported by php-webdriver, so we must force Chromedriver to // not use the W3C protocol by default (which is what Chromedriver does starting with version 75). - if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME) { + if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME + && mb_strpos($selenium_server_url, 'browserstack') === false // see https://github.com/facebook/php-webdriver/issues/644 + ) { $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY); $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions(); From e43de70f3c7166169d0f14a374505392734160e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Thu, 13 Jun 2019 10:02:18 +0200 Subject: [PATCH 023/373] Release version 1.7.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5487916..a5e4ca748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## 1.7.1 - 2019-06-13 +### Fixed +- Error `Call to a member function toArray()` if capabilities were already converted to an array. +- Temporarily do not send capabilities to disable W3C WebDriver protocol when BrowserStack hub is used. + ## 1.7.0 - 2019-06-10 ### Added - `WebDriverCheckboxes` and `WebDriverRadios` helper classes to simplify interaction with checkboxes and radio buttons. From 3f73de8364e7285115750e7e47b9a27e08c730d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 14 Jun 2019 11:33:44 +0200 Subject: [PATCH 024/373] Revert Chromedriver bug workaround, which is fixed in Chromedriver 75.0.3770.90 --- .travis.yml | 2 +- CHANGELOG.md | 3 +++ lib/Remote/HttpCommandExecutor.php | 5 ----- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index b6850baf1..ac6cc1e24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,7 @@ matrix: # Stable Chrome + Chromedriver 75+ inside Travis environment - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.8" + env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.90" addons: chrome: stable diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e4ca748..f1e733516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Changed +- Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). + ## 1.7.1 - 2019-06-13 ### Fixed - Error `Call to a member function toArray()` if capabilities were already converted to an array. diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index 1891fdb35..f584324b7 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -273,11 +273,6 @@ public function execute(WebDriverCommand $command) if ($http_method === 'POST' && $params && is_array($params)) { $encoded_params = json_encode($params); - } elseif ($http_method === 'POST' && $encoded_params === null) { - // Workaround for bug https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943 in Chrome 75. - // Chromedriver now erroneously does not allow POST body to be empty even for the JsonWire protocol. - // If the command POST is empty, here we send some dummy data as a workaround: - $encoded_params = json_encode(['_' => '_']); } curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params); From 4f8b32289daa41c9c96b6a9dbfcb352a24e5af1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 1 Nov 2019 19:16:13 +0000 Subject: [PATCH 025/373] Download latest chromedriver without a need of manual version bumping --- .travis.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index ac6cc1e24..37dfc2cf1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,15 +39,9 @@ matrix: addons: firefox: "45.8.0esr" - # Stable Chrome + Chromedriver 74 inside Travis environment + # Stable Chrome + Chromedriver inside Travis environment - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="74.0.3729.6" - addons: - chrome: stable - - # Stable Chrome + Chromedriver 75+ inside Travis environment - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" CHROMEDRIVER_VERSION="75.0.3770.90" + env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" addons: chrome: stable @@ -102,7 +96,7 @@ install: - travis_retry composer update --no-interaction $DEPENDENCIES before_script: - - if [ "$BROWSER_NAME" = "chrome" ]; then mkdir chromedriver; wget -q -t 3 https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip; unzip chromedriver_linux64 -d chromedriver; fi + - if [ "$BROWSER_NAME" = "chrome" ]; then 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 - 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 From 87be63361d6670b8110b7d084887746b1adc685b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 9 Apr 2018 16:25:57 +0200 Subject: [PATCH 026/373] W3C-compatible implementation --- .travis.yml | 12 +- CONTRIBUTING.md | 10 +- lib/AbstractWebDriverCheckboxOrRadio.php | 2 +- lib/Cookie.php | 8 +- lib/Exception/WebDriverException.php | 50 ++++- lib/Interactions/WebDriverActions.php | 1 - lib/Remote/DriverCommand.php | 4 + lib/Remote/HttpCommandExecutor.php | 63 ++++++- lib/Remote/JsonWireCompat.php | 102 ++++++++++ lib/Remote/RemoteMouse.php | 174 +++++++++++++++++- lib/Remote/RemoteTargetLocator.php | 4 +- lib/Remote/RemoteWebDriver.php | 82 ++++++--- lib/Remote/RemoteWebElement.php | 112 ++++++++--- lib/WebDriverDimension.php | 12 +- lib/WebDriverOptions.php | 9 +- lib/WebDriverPoint.php | 4 +- lib/WebDriverTimeouts.php | 34 +++- .../functional/RemoteWebDriverCreateTest.php | 5 +- .../RemoteWebDriverFindElementTest.php | 20 ++ tests/functional/RemoteWebDriverTest.php | 23 ++- tests/functional/RemoteWebElementTest.php | 4 + tests/functional/WebDriverActionsTest.php | 24 ++- tests/functional/WebDriverTestCase.php | 7 +- tests/functional/web/escape_css.html | 14 ++ tests/functional/web/upload.html | 2 +- 25 files changed, 699 insertions(+), 83 deletions(-) create mode 100644 lib/Remote/JsonWireCompat.php create mode 100644 tests/functional/web/escape_css.html diff --git a/.travis.yml b/.travis.yml index 37dfc2cf1..47ddb461e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a01c7adb..741a140a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,10 +37,10 @@ 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: @@ -48,6 +48,12 @@ 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: diff --git a/lib/AbstractWebDriverCheckboxOrRadio.php b/lib/AbstractWebDriverCheckboxOrRadio.php index f70a2ef04..1268ce7c0 100644 --- a/lib/AbstractWebDriverCheckboxOrRadio.php +++ b/lib/AbstractWebDriverCheckboxOrRadio.php @@ -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) )); diff --git a/lib/Cookie.php b/lib/Cookie.php index 216ac3033..170b46486 100644 --- a/lib/Cookie.php +++ b/lib/Cookie.php @@ -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 + $cookie['secure'] = false; + } + + return $cookie; } public function offsetExists($offset) diff --git a/lib/Exception/WebDriverException.php b/lib/Exception/WebDriverException.php index 8545729ad..b17b2f70f 100644 --- a/lib/Exception/WebDriverException.php +++ b/lib/Exception/WebDriverException.php @@ -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 * @@ -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); diff --git a/lib/Interactions/WebDriverActions.php b/lib/Interactions/WebDriverActions.php index e97796854..66ac7c1ab 100644 --- a/lib/Interactions/WebDriverActions.php +++ b/lib/Interactions/WebDriverActions.php @@ -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; diff --git a/lib/Remote/DriverCommand.php b/lib/Remote/DriverCommand.php index c69052030..fd24a691c 100644 --- a/lib/Remote/DriverCommand.php +++ b/lib/Remote/DriverCommand.php @@ -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() { } diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index f584324b7..c783b2892 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -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 */ @@ -145,6 +168,10 @@ class HttpCommandExecutor implements WebDriverCommandExecutor * @var resource */ protected $curl; + /** + * @var bool + */ + protected $w3cCompliant = true; /** * @param string $url @@ -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(); @@ -179,6 +208,11 @@ public function __construct($url, $http_proxy = null, $http_proxy_port = null) $this->setConnectionTimeout(30000); } + public function disableW3CCompliance() + { + $this->w3cCompliant = false; + } + /** * Set timeout for the connect phase * @@ -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->w3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { + throw new InvalidArgumentException($command->getName() . ' is not a valid command.'); + } + } + + if ($this->w3cCompliant) { + $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); @@ -317,12 +359,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); } diff --git a/lib/Remote/JsonWireCompat.php b/lib/Remote/JsonWireCompat.php new file mode 100644 index 000000000..78b9cba0e --- /dev/null +++ b/lib/Remote/JsonWireCompat.php @@ -0,0 +1,102 @@ +getMechanism(); + $value = $by->getValue(); + + if ($w3cCompliant) { + switch ($mechanism) { + // Convert to CSS selectors + case 'class name': + $mechanism = 'css selector'; + $value = sprintf('.%s', self::escapeSelector($value)); + break; + case 'id': + $mechanism = 'css selector'; + $value = sprintf('#%s', self::escapeSelector($value)); + break; + case 'name': + $mechanism = 'css selector'; + $value = sprintf('[name=\'%s\']', self::escapeSelector($value)); + break; + } + } + + return ['using' => $mechanism, 'value' => $value]; + } + + /** + * Escapes a CSS selector. + * + * Code adapted from the Zend Escaper project. + * + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @see https://github.com/zendframework/zend-escaper/blob/master/src/Escaper.php + * + * @param string $selector + * @return string + */ + private static function escapeSelector($selector) + { + return preg_replace_callback('/[^a-z0-9]/iSu', function ($matches) { + $chr = $matches[0]; + if (mb_strlen($chr) == 1) { + $ord = ord($chr); + } else { + $chr = mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); + $ord = hexdec(bin2hex($chr)); + } + + return sprintf('\\%X ', $ord); + }, $selector); + } +} diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php index 48c578df7..aa0d59829 100644 --- a/lib/Remote/RemoteMouse.php +++ b/lib/Remote/RemoteMouse.php @@ -27,13 +27,19 @@ class RemoteMouse implements WebDriverMouse * @var RemoteExecuteMethod */ private $executor; + /** + * @var bool + */ + private $w3cCompliant; /** * @param RemoteExecuteMethod $executor + * @param bool $w3cCompliant */ - public function __construct(RemoteExecuteMethod $executor) + public function __construct(RemoteExecuteMethod $executor, $w3cCompliant = false) { $this->executor = $executor; + $this->w3cCompliant = $w3cCompliant; } /** @@ -43,6 +49,22 @@ public function __construct(RemoteExecuteMethod $executor) */ public function click(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, $this->createClickActions()), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::CLICK, [ 'button' => 0, @@ -58,6 +80,33 @@ public function click(WebDriverCoordinates $where = null) */ public function contextClick(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, [ + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 2, + ], + [ + 'type' => 'pointerUp', + 'duration' => 0, + 'button' => 2, + ], + ]), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::CLICK, [ 'button' => 2, @@ -73,6 +122,23 @@ public function contextClick(WebDriverCoordinates $where = null) */ public function doubleClick(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $clickActions = $this->createClickActions(); + $moveAction = null === $where ? [] : [$this->createMoveAction($where)]; + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, $clickActions, $clickActions), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::DOUBLE_CLICK); @@ -86,6 +152,28 @@ public function doubleClick(WebDriverCoordinates $where = null) */ public function mouseDown(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => [ + $this->createMoveAction($where), + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 0, + ], + ], + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::MOUSE_DOWN); @@ -104,6 +192,21 @@ public function mouseMove( $x_offset = null, $y_offset = null ) { + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => [$this->createMoveAction($where, $x_offset, $y_offset)], + ], + ], + ]); + + return $this; + } + $params = []; if ($where !== null) { $params['element'] = $where->getAuxiliary(); @@ -114,6 +217,7 @@ public function mouseMove( if ($y_offset !== null) { $params['yoffset'] = $y_offset; } + $this->executor->execute(DriverCommand::MOVE_TO, $params); return $this; @@ -126,6 +230,29 @@ public function mouseMove( */ public function mouseUp(WebDriverCoordinates $where = null) { + if ($this->w3cCompliant) { + $moveAction = $where ? [$this->createMoveAction($where)] : []; + + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [ + [ + 'type' => 'pointer', + 'id' => 'mouse', + 'parameters' => ['pointerType' => 'mouse'], + 'actions' => array_merge($moveAction, [ + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 0, + ], + ]), + ], + ], + ]); + + return $this; + } + $this->moveIfNeeded($where); $this->executor->execute(DriverCommand::MOUSE_UP); @@ -141,4 +268,49 @@ protected function moveIfNeeded(WebDriverCoordinates $where = null) $this->mouseMove($where); } } + + /** + * @param WebDriverCoordinates $where + * @param int|null $x_offset + * @param int|null $y_offset + * + * @return array + */ + private function createMoveAction( + WebDriverCoordinates $where = null, + $x_offset = null, + $y_offset = null + ) { + $move_action = [ + 'type' => 'pointerMove', + 'duration' => 0, + 'x' => $x_offset === null ? 0 : $x_offset, + 'y' => $y_offset === null ? 0 : $y_offset, + ]; + + if ($where !== null) { + $move_action['origin'] = [JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $where->getAuxiliary()]; + } + + return $move_action; + } + + /** + * @return array + */ + private function createClickActions() + { + return [ + [ + 'type' => 'pointerDown', + 'duration' => 0, + 'button' => 0, + ], + [ + 'type' => 'pointerUp', + 'duration' => 0, + 'button' => 0, + ], + ]; + } } diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 5c218075d..55a879c0d 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -112,6 +112,8 @@ public function activeElement() $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); - return new RemoteWebElement($method, $response['ELEMENT']); + $w3cCompliant = $this->driver instanceof RemoteWebDriver ? $this->driver->isW3cCompliant() : false; + + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $w3cCompliant); } } diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 71f45704e..6d3a7d833 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -59,19 +59,26 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu * @var RemoteExecuteMethod */ protected $executeMethod; + /** + * @var bool + */ + protected $w3cCompliant; /** * @param HttpCommandExecutor $commandExecutor * @param string $sessionId * @param WebDriverCapabilities|null $capabilities + * @param bool $w3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec */ protected function __construct( HttpCommandExecutor $commandExecutor, $sessionId, - WebDriverCapabilities $capabilities = null + WebDriverCapabilities $capabilities = null, + $w3cCompliant = false ) { $this->executor = $commandExecutor; $this->sessionID = $sessionId; + $this->w3cCompliant = $w3cCompliant; if ($capabilities !== null) { $this->capabilities = $capabilities; @@ -88,6 +95,7 @@ protected function __construct( * @param string|null $http_proxy The proxy to tunnel requests to the remote Selenium WebDriver through * @param int|null $http_proxy_port The proxy port to tunnel requests to the remote Selenium WebDriver through * @param DesiredCapabilities $required_capabilities The required capabilities + * * @return static */ public static function create( @@ -99,6 +107,7 @@ public static function create( $http_proxy_port = null, DesiredCapabilities $required_capabilities = null ) { + // BC layer to not break the method signature $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url); $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); @@ -128,23 +137,43 @@ public static function create( $executor->setRequestTimeout($request_timeout_in_ms); } + // W3C + $parameters = [ + 'capabilities' => [ + 'firstMatch' => [$desired_capabilities->toArray()], + ], + ]; + + // Legacy protocol + if (null !== $required_capabilities && $required_capabilities_array = $required_capabilities->toArray()) { + $parameters['capabilities']['alwaysMatch'] = $required_capabilities_array; + } + if ($required_capabilities !== null) { // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. - // This will probably change in future with the W3C WebDriver spec, but is the only way how to pass these - // values now. + // This has changed with the W3C WebDriver spec, but is the only way how to pass these + // values with the legacy protocol. $desired_capabilities->setCapability('requiredCapabilities', $required_capabilities->toArray()); } + $parameters['desiredCapabilities'] = $desired_capabilities->toArray(); + $command = new WebDriverCommand( null, DriverCommand::NEW_SESSION, - ['desiredCapabilities' => $desired_capabilities->toArray()] + $parameters ); $response = $executor->execute($command); - $returnedCapabilities = new DesiredCapabilities($response->getValue()); + $value = $response->getValue(); + + if (!$w3c_compliant = isset($value['capabilities'])) { + $executor->disableW3CCompliance(); + } - $driver = new static($executor, $response->getSessionID(), $returnedCapabilities); + $returnedCapabilities = new DesiredCapabilities($w3c_compliant ? $value['capabilities'] : $value); + + $driver = new static($executor, $response->getSessionID(), $returnedCapabilities, $w3c_compliant); return $driver; } @@ -167,7 +196,9 @@ public static function createBySessionID( $connection_timeout_in_ms = null, $request_timeout_in_ms = null ) { - $executor = new HttpCommandExecutor($selenium_server_url); + // BC layer to not break the method signature + $w3c_compliant = func_num_args() > 3 ? func_get_arg(3) : false; + $executor = new HttpCommandExecutor($selenium_server_url, null, null); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); } @@ -175,7 +206,7 @@ public static function createBySessionID( $executor->setRequestTimeout($request_timeout_in_ms); } - return new static($executor, $session_id); + return new static($executor, $session_id, null, $w3c_compliant); } /** @@ -199,13 +230,12 @@ public function close() */ public function findElement(WebDriverBy $by) { - $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()]; $raw_element = $this->execute( DriverCommand::FIND_ELEMENT, - $params + JsonWireCompat::getUsing($by, $this->w3cCompliant) ); - return $this->newElement($raw_element['ELEMENT']); + return $this->newElement(JsonWireCompat::getElement($raw_element)); } /** @@ -217,15 +247,14 @@ public function findElement(WebDriverBy $by) */ public function findElements(WebDriverBy $by) { - $params = ['using' => $by->getMechanism(), 'value' => $by->getValue()]; $raw_elements = $this->execute( DriverCommand::FIND_ELEMENTS, - $params + JsonWireCompat::getUsing($by, $this->w3cCompliant) ); $elements = []; foreach ($raw_elements as $raw_element) { - $elements[] = $this->newElement($raw_element['ELEMENT']); + $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); } return $elements; @@ -399,7 +428,7 @@ public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) */ public function manage() { - return new WebDriverOptions($this->getExecuteMethod()); + return new WebDriverOptions($this->getExecuteMethod(), $this->w3cCompliant); } /** @@ -430,7 +459,7 @@ public function switchTo() public function getMouse() { if (!$this->mouse) { - $this->mouse = new RemoteMouse($this->getExecuteMethod()); + $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->w3cCompliant); } return $this->mouse; @@ -541,7 +570,7 @@ public function getCapabilities() */ public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000) { - $executor = new HttpCommandExecutor($selenium_server_url); + $executor = new HttpCommandExecutor($selenium_server_url, null, null); $executor->setConnectionTimeout($timeout_in_ms); $command = new WebDriverCommand( @@ -570,6 +599,15 @@ public function execute($command_name, $params = []) return null; } + /** + * @internal + * @return bool + */ + public function isW3cCompliant() + { + return $this->w3cCompliant; + } + /** * Prepare arguments for JavaScript injection * @@ -581,9 +619,11 @@ protected function prepareScriptArguments(array $arguments) $args = []; foreach ($arguments as $key => $value) { if ($value instanceof WebDriverElement) { - $args[$key] = ['ELEMENT' => $value->getID()]; + $args[$key] = [ + $this->w3cCompliant ? JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER : 'ELEMENT' => $value->getID(), + ]; } else { - if (is_array($value)) { + if (\is_array($value)) { $value = $this->prepareScriptArguments($value); } $args[$key] = $value; @@ -613,7 +653,7 @@ protected function getExecuteMethod() */ protected function newElement($id) { - return new RemoteWebElement($this->getExecuteMethod(), $id); + return new RemoteWebElement($this->getExecuteMethod(), $id, $this->w3cCompliant); } /** @@ -629,7 +669,7 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return new DesiredCapabilities(); } - if (is_array($desired_capabilities)) { + if (\is_array($desired_capabilities)) { return new DesiredCapabilities($desired_capabilities); } diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php index 26936d8b9..b07b18227 100644 --- a/lib/Remote/RemoteWebElement.php +++ b/lib/Remote/RemoteWebElement.php @@ -15,6 +15,7 @@ namespace Facebook\WebDriver\Remote; +use Facebook\WebDriver\Exception\UnsupportedOperationException; use Facebook\WebDriver\Exception\WebDriverException; use Facebook\WebDriver\Interactions\Internal\WebDriverCoordinates; use Facebook\WebDriver\Internal\WebDriverLocatable; @@ -42,16 +43,22 @@ class RemoteWebElement implements WebDriverElement, WebDriverLocatable * @var UselessFileDetector */ protected $fileDetector; + /** + * @var bool + */ + protected $w3cCompliant; /** * @param RemoteExecuteMethod $executor * @param string $id + * @param bool $w3cCompliant */ - public function __construct(RemoteExecuteMethod $executor, $id) + public function __construct(RemoteExecuteMethod $executor, $id, $w3cCompliant = false) { $this->executor = $executor; $this->id = $id; $this->fileDetector = new UselessFileDetector(); + $this->w3cCompliant = $w3cCompliant; } /** @@ -66,6 +73,23 @@ public function clear() [':id' => $this->id] ); + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::ACTIONS, [ + 'actions' => [[ + 'type' => 'key', + 'id' => 'keyboard', + 'actions' => [ + ['type' => 'keyDown' , 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyDown' , 'value' => 'a'], + ['type' => 'keyUp' , 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyUp' , 'value' => 'a'], + ['type' => 'keyDown' , 'value' => WebDriverKeys::BACKSPACE], + ['type' => 'keyUp' , 'value' => WebDriverKeys::BACKSPACE], + ], + ]], + ]); + } + return $this; } @@ -94,17 +118,15 @@ public function click() */ public function findElement(WebDriverBy $by) { - $params = [ - 'using' => $by->getMechanism(), - 'value' => $by->getValue(), - ':id' => $this->id, - ]; + $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params[':id'] = $this->id; + $raw_element = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENT, $params ); - return $this->newElement($raw_element['ELEMENT']); + return $this->newElement(JsonWireCompat::getElement($raw_element)); } /** @@ -117,11 +139,8 @@ public function findElement(WebDriverBy $by) */ public function findElements(WebDriverBy $by) { - $params = [ - 'using' => $by->getMechanism(), - 'value' => $by->getValue(), - ':id' => $this->id, - ]; + $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params[':id'] = $this->id; $raw_elements = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENTS, $params @@ -129,7 +148,7 @@ public function findElements(WebDriverBy $by) $elements = []; foreach ($raw_elements as $raw_element) { - $elements[] = $this->newElement($raw_element['ELEMENT']); + $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element)); } return $elements; @@ -148,10 +167,23 @@ public function getAttribute($attribute_name) ':id' => $this->id, ]; - return $this->executor->execute( - DriverCommand::GET_ELEMENT_ATTRIBUTE, - $params - ); + if ($this->w3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { + $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); + + if ($value === true) { + return 'true'; + } + + if ($value === false) { + return 'false'; + } + + if ($value !== null) { + return (string) $value; + } + } + + return $this->executor->execute(DriverCommand::GET_ELEMENT_ATTRIBUTE, $params); } /** @@ -325,20 +357,37 @@ public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); if ($local_file === null) { + if ($this->w3cCompliant) { + $params = [ + 'text' => (string) $value, + ':id' => $this->id, + ]; + } else { + $params = [ + 'value' => WebDriverKeys::encode($value), + ':id' => $this->id, + ]; + } + + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); + + return $this; + } + + if ($this->w3cCompliant) { $params = [ - 'value' => WebDriverKeys::encode($value), + 'text' => $local_file, ':id' => $this->id, ]; - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); } else { - $remote_path = $this->upload($local_file); $params = [ - 'value' => WebDriverKeys::encode($remote_path), + 'value' => WebDriverKeys::encode($this->upload($local_file)), ':id' => $this->id, ]; - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); } + $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); + return $this; } @@ -371,6 +420,19 @@ public function setFileDetector(FileDetector $detector) */ public function submit() { + if ($this->w3cCompliant) { + $this->executor->execute(DriverCommand::EXECUTE_SCRIPT, [ + // cannot call the submit method directly in case an input of this form is named "submit" + 'script' => sprintf( + 'return Object.getPrototypeOf(%1$s).submit.call(%1$s);', + 'form' === $this->getTagName() ? 'arguments[0]' : 'arguments[0].form' + ), + 'args' => [[JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER => $this->id]], + ]); + + return $this; + } + $this->executor->execute( DriverCommand::SUBMIT_ELEMENT, [':id' => $this->id] @@ -397,6 +459,10 @@ public function getID() */ public function equals(WebDriverElement $other) { + if ($this->w3cCompliant) { + throw new UnsupportedOperationException('"elementEquals" is not supported by the W3C specification'); + } + return $this->executor->execute(DriverCommand::ELEMENT_EQUALS, [ ':id' => $this->id, ':other' => $other->getID(), @@ -412,7 +478,7 @@ public function equals(WebDriverElement $other) */ protected function newElement($id) { - return new static($this->executor, $id); + return new static($this->executor, $id, $this->w3cCompliant); } /** diff --git a/lib/WebDriverDimension.php b/lib/WebDriverDimension.php index 308c1990f..81a223989 100644 --- a/lib/WebDriverDimension.php +++ b/lib/WebDriverDimension.php @@ -21,17 +21,17 @@ class WebDriverDimension { /** - * @var int + * @var int|float */ private $height; /** - * @var int + * @var int|float */ private $width; /** - * @param int $width - * @param int $height + * @param int|float $width + * @param int|float $height */ public function __construct($width, $height) { @@ -46,7 +46,7 @@ public function __construct($width, $height) */ public function getHeight() { - return $this->height; + return (int) $this->height; } /** @@ -56,7 +56,7 @@ public function getHeight() */ public function getWidth() { - return $this->width; + return (int) $this->width; } /** diff --git a/lib/WebDriverOptions.php b/lib/WebDriverOptions.php index bec7fd18b..4c584bd07 100644 --- a/lib/WebDriverOptions.php +++ b/lib/WebDriverOptions.php @@ -28,10 +28,15 @@ class WebDriverOptions * @var ExecuteMethod */ protected $executor; + /** + * @var bool + */ + protected $w3cCompliant; - public function __construct(ExecuteMethod $executor) + public function __construct(ExecuteMethod $executor, $w3cCompliant = false) { $this->executor = $executor; + $this->w3cCompliant = $w3cCompliant; } /** @@ -128,7 +133,7 @@ public function getCookies() */ public function timeouts() { - return new WebDriverTimeouts($this->executor); + return new WebDriverTimeouts($this->executor, $this->w3cCompliant); } /** diff --git a/lib/WebDriverPoint.php b/lib/WebDriverPoint.php index 4e2dbd211..9600af9d8 100644 --- a/lib/WebDriverPoint.php +++ b/lib/WebDriverPoint.php @@ -36,7 +36,7 @@ public function __construct($x, $y) */ public function getX() { - return $this->x; + return (int) $this->x; } /** @@ -46,7 +46,7 @@ public function getX() */ public function getY() { - return $this->y; + return (int) $this->y; } /** diff --git a/lib/WebDriverTimeouts.php b/lib/WebDriverTimeouts.php index 6903f7080..eeba9a10e 100644 --- a/lib/WebDriverTimeouts.php +++ b/lib/WebDriverTimeouts.php @@ -27,10 +27,15 @@ class WebDriverTimeouts * @var ExecuteMethod */ protected $executor; + /** + * @var bool + */ + protected $w3cCompliant; - public function __construct(ExecuteMethod $executor) + public function __construct(ExecuteMethod $executor, $w3cCompliant = false) { $this->executor = $executor; + $this->w3cCompliant = $w3cCompliant; } /** @@ -41,6 +46,15 @@ public function __construct(ExecuteMethod $executor) */ public function implicitlyWait($seconds) { + if ($this->w3cCompliant) { + $this->executor->execute( + DriverCommand::IMPLICITLY_WAIT, + ['implicit' => $seconds * 1000] + ); + + return $this; + } + $this->executor->execute( DriverCommand::IMPLICITLY_WAIT, ['ms' => $seconds * 1000] @@ -57,6 +71,15 @@ public function implicitlyWait($seconds) */ public function setScriptTimeout($seconds) { + if ($this->w3cCompliant) { + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['script' => $seconds * 1000] + ); + + return $this; + } + $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['ms' => $seconds * 1000] @@ -73,6 +96,15 @@ public function setScriptTimeout($seconds) */ public function pageLoadTimeout($seconds) { + if ($this->w3cCompliant) { + $this->executor->execute( + DriverCommand::SET_SCRIPT_TIMEOUT, + ['pageLoad' => $seconds * 1000] + ); + + return $this; + } + $this->executor->execute(DriverCommand::SET_TIMEOUT, [ 'type' => 'page load', 'ms' => $seconds * 1000, diff --git a/tests/functional/RemoteWebDriverCreateTest.php b/tests/functional/RemoteWebDriverCreateTest.php index 90c072922..a2f6e9d0f 100644 --- a/tests/functional/RemoteWebDriverCreateTest.php +++ b/tests/functional/RemoteWebDriverCreateTest.php @@ -33,7 +33,10 @@ public function testShouldStartBrowserAndCreateInstanceOfRemoteWebDriver() $this->serverUrl, $this->desiredCapabilities, $this->connectionTimeout, - $this->requestTimeout + $this->requestTimeout, + null, + null, + null ); $this->assertInstanceOf(RemoteWebDriver::class, $this->driver); diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index eea14b6e5..066179f1f 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -61,4 +61,24 @@ public function testShouldFindMultipleElements() $this->assertCount(5, $elements); $this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements); } + + public function testEscapeCssSelector() + { + if (getenv('GECKODRIVER') !== '1') { + $this->markTestSkipped( + 'CSS selectors containing special characters are not supported by the legacy protocol' + ); + } + + $this->driver->get($this->getTestPageUrl('escape_css.html')); + + $element = $this->driver->findElement(WebDriverBy::id('.fo\'oo')); + $this->assertSame('Foo', $element->getText()); + + $element = $this->driver->findElement(WebDriverBy::className('#ba\'r')); + $this->assertSame('Bar', $element->getText()); + + $element = $this->driver->findElement(WebDriverBy::name('.#ba\'z')); + $this->assertSame('Baz', $element->getText()); + } } diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index d996d304a..9105aa93f 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -77,7 +77,11 @@ public function testShouldGetSessionId() */ public function testShouldGetAllSessions() { - $sessions = RemoteWebDriver::getAllSessions($this->serverUrl); + if (getenv('GECKODRIVER') === '1') { + $this->markTestSkipped('"getAllSessions" is not supported by the W3C specification'); + } + + $sessions = RemoteWebDriver::getAllSessions($this->serverUrl, 30000); $this->assertInternalType('array', $sessions); $this->assertCount(1, $sessions); @@ -94,12 +98,22 @@ public function testShouldGetAllSessions() */ public function testShouldQuitAndUnsetExecutor() { - $this->assertCount(1, RemoteWebDriver::getAllSessions($this->serverUrl)); + if (getenv('GECKODRIVER') === '1') { + $this->markTestSkipped('"getAllSessions" is not supported by the W3C specification'); + } + + $this->assertCount( + 1, + RemoteWebDriver::getAllSessions($this->serverUrl, 30000) + ); $this->assertInstanceOf(HttpCommandExecutor::class, $this->driver->getCommandExecutor()); $this->driver->quit(); - $this->assertCount(0, RemoteWebDriver::getAllSessions($this->serverUrl)); + $this->assertCount( + 0, + RemoteWebDriver::getAllSessions($this->serverUrl, 30000) + ); $this->assertNull($this->driver->getCommandExecutor()); } @@ -136,6 +150,9 @@ public function testShouldCloseWindow() $this->driver->get($this->getTestPageUrl('open_new_window.html')); $this->driver->findElement(WebDriverBy::cssSelector('a'))->click(); + // Mandatory for Geckodriver + $this->driver->wait()->until(WebDriverExpectedCondition::numberOfWindowsToBe(2)); + $this->assertCount(2, $this->driver->getWindowHandles()); $this->driver->close(); diff --git a/tests/functional/RemoteWebElementTest.php b/tests/functional/RemoteWebElementTest.php index d4614a431..a4684ace3 100644 --- a/tests/functional/RemoteWebElementTest.php +++ b/tests/functional/RemoteWebElementTest.php @@ -272,6 +272,10 @@ public function testShouldSubmitFormByClickOnSubmitInput() */ public function testShouldCompareEqualsElement() { + if (getenv('GECKODRIVER') === '1') { + $this->markTestSkipped('"equals" is not supported by the W3C specification'); + } + $this->driver->get($this->getTestPageUrl('index.html')); $firstElement = $this->driver->findElement(WebDriverBy::cssSelector('ul.list')); diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 3df984810..b2bd51835 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -46,10 +46,16 @@ public function testShouldClickOnElement() ->click($element) ->perform(); - $this->assertSame( - ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], - $this->retrieveLoggedEvents() - ); + $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + $loggedEvents = $this->retrieveLoggedEvents(); + + if ('1' === getenv('GECKODRIVER')) { + $loggedEvents = array_slice($loggedEvents, 0, count($logs)); + // Firefox sometimes triggers some extra events + // it's not related to Geckodriver, it's Firefox's own behavior + } + + $this->assertSame($logs, $loggedEvents); } /** @@ -71,10 +77,12 @@ public function testShouldClickAndHoldOnElementAndRelease() ->release() ->perform(); - $this->assertSame( - ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], - $this->retrieveLoggedEvents() - ); + if ('1' === getenv('GECKODRIVER')) { + $logs = ['mouseover item-1', 'mousedown item-1', 'dragstart item-1']; + } else { + $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + } + $this->assertSame($logs, $this->retrieveLoggedEvents()); } /** diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 5fe6a7e8a..7a0f8af23 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -58,6 +58,8 @@ protected function setUp() // --no-sandbox is a workaround for Chrome crashing: https://github.com/SeleniumHQ/selenium/issues/4961 $chromeOptions->addArguments(['--headless', 'window-size=1024,768', '--no-sandbox']); $this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); + } elseif (getenv('GECKODRIVER') === '1') { + $this->serverUrl = 'http://localhost:4444/'; } $this->desiredCapabilities->setBrowserName($browserName); @@ -68,7 +70,10 @@ protected function setUp() $this->serverUrl, $this->desiredCapabilities, $this->connectionTimeout, - $this->requestTimeout + $this->requestTimeout, + null, + null, + null ); } } diff --git a/tests/functional/web/escape_css.html b/tests/functional/web/escape_css.html new file mode 100644 index 000000000..48213084a --- /dev/null +++ b/tests/functional/web/escape_css.html @@ -0,0 +1,14 @@ + + + + + Test CSS selector escaping + + + +
Foo
+
Bar
+
Baz
+ + + diff --git a/tests/functional/web/upload.html b/tests/functional/web/upload.html index 65a622c50..6762873c3 100644 --- a/tests/functional/web/upload.html +++ b/tests/functional/web/upload.html @@ -8,7 +8,7 @@

- +

From 4a65bed00dd2bdbbfa81206590df29d2060221c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Aug 2019 16:18:50 +0200 Subject: [PATCH 027/373] Add back Geckodriver to the Travis matrix --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 47ddb461e..906951171 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,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.24.0/geckodriver-v0.24.0-linux64.tar.gz; tar xzf geckodriver-v0.24.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" @@ -124,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 From 5729355c5d37953d6a1a5a2f55afc7ba1c23bf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 20 Aug 2019 17:06:26 +0200 Subject: [PATCH 028/373] Improve tests stability --- tests/functional/WebDriverActionsTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index b2bd51835..1f03c8899 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -78,11 +78,13 @@ public function testShouldClickAndHoldOnElementAndRelease() ->perform(); if ('1' === getenv('GECKODRIVER')) { - $logs = ['mouseover item-1', 'mousedown item-1', 'dragstart item-1']; + $this->assertArraySubset(['mouseover item-1', 'mousedown item-1'], $this->retrieveLoggedEvents()); } else { - $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; + $this->assertSame( + ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1'], + $this->retrieveLoggedEvents() + ); } - $this->assertSame($logs, $this->retrieveLoggedEvents()); } /** From 721a7c2737ea8353c01e76ec7752aa0751e667eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 4 Nov 2019 19:36:50 +0000 Subject: [PATCH 029/373] Use latest geckodriver --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 906951171..950e8fe64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,7 +108,7 @@ 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.24.0/geckodriver-v0.24.0-linux64.tar.gz; tar xzf geckodriver-v0.24.0-linux64.tar.gz -C geckodriver; 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 - if [ "$GECKODRIVER" = "1" ]; then From 71a6698227485b368d4212980ed9efcde15b292f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 5 Nov 2019 15:35:40 +0000 Subject: [PATCH 030/373] Make POST body always valid JSON in W3C mode --- lib/Remote/HttpCommandExecutor.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index c783b2892..8114d7817 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -313,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->w3cCompliant) { + // 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); From 7119d7fc97c5291768cd8632a15bfa7d75bff145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 5 Nov 2019 15:37:36 +0000 Subject: [PATCH 031/373] Codestyle: unify casing in w3c methods --- lib/Remote/HttpCommandExecutor.php | 12 +++---- lib/Remote/JsonWireCompat.php | 8 ++--- lib/Remote/RemoteMouse.php | 20 ++++++------ lib/Remote/RemoteTargetLocator.php | 4 +-- lib/Remote/RemoteWebDriver.php | 30 +++++++++--------- lib/Remote/RemoteWebElement.php | 50 ++++++++++++++++-------------- lib/WebDriverOptions.php | 8 ++--- lib/WebDriverTimeouts.php | 12 +++---- 8 files changed, 74 insertions(+), 70 deletions(-) diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index 8114d7817..b87c94069 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -171,7 +171,7 @@ class HttpCommandExecutor implements WebDriverCommandExecutor /** * @var bool */ - protected $w3cCompliant = true; + protected $isW3cCompliant = true; /** * @param string $url @@ -208,9 +208,9 @@ public function __construct($url, $http_proxy = null, $http_proxy_port = null) $this->setConnectionTimeout(30000); } - public function disableW3CCompliance() + public function disableW3cCompliance() { - $this->w3cCompliant = false; + $this->isW3cCompliant = false; } /** @@ -262,12 +262,12 @@ public function execute(WebDriverCommand $command) { $commandName = $command->getName(); if (!isset(self::$commands[$commandName])) { - if ($this->w3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { + if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) { throw new InvalidArgumentException($command->getName() . ' is not a valid command.'); } } - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $raw = self::$w3cCompliantCommands[$command->getName()]; } else { $raw = self::$commands[$command->getName()]; @@ -316,7 +316,7 @@ public function execute(WebDriverCommand $command) if ($http_method === 'POST') { if ($params && is_array($params)) { $encoded_params = json_encode($params); - } elseif ($this->w3cCompliant) { + } elseif ($this->isW3cCompliant) { // POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model $encoded_params = '{}'; } diff --git a/lib/Remote/JsonWireCompat.php b/lib/Remote/JsonWireCompat.php index 78b9cba0e..3488b8604 100644 --- a/lib/Remote/JsonWireCompat.php +++ b/lib/Remote/JsonWireCompat.php @@ -44,16 +44,16 @@ public static function getElement(array $rawElement) /** * @param WebDriverBy $by - * @param bool $w3cCompliant + * @param bool $isW3cCompliant * * @return array */ - public static function getUsing(WebDriverBy $by, $w3cCompliant) + public static function getUsing(WebDriverBy $by, $isW3cCompliant) { $mechanism = $by->getMechanism(); $value = $by->getValue(); - if ($w3cCompliant) { + if ($isW3cCompliant) { switch ($mechanism) { // Convert to CSS selectors case 'class name': @@ -89,7 +89,7 @@ private static function escapeSelector($selector) { return preg_replace_callback('/[^a-z0-9]/iSu', function ($matches) { $chr = $matches[0]; - if (mb_strlen($chr) == 1) { + if (mb_strlen($chr) === 1) { $ord = ord($chr); } else { $chr = mb_convert_encoding($chr, 'UTF-32BE', 'UTF-8'); diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php index aa0d59829..4bd4a849b 100644 --- a/lib/Remote/RemoteMouse.php +++ b/lib/Remote/RemoteMouse.php @@ -30,16 +30,16 @@ class RemoteMouse implements WebDriverMouse /** * @var bool */ - private $w3cCompliant; + private $isW3cCompliant; /** * @param RemoteExecuteMethod $executor - * @param bool $w3cCompliant + * @param bool $isW3cCompliant */ - public function __construct(RemoteExecuteMethod $executor, $w3cCompliant = false) + public function __construct(RemoteExecuteMethod $executor, $isW3cCompliant = false) { $this->executor = $executor; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -49,7 +49,7 @@ public function __construct(RemoteExecuteMethod $executor, $w3cCompliant = false */ public function click(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $moveAction = $where ? [$this->createMoveAction($where)] : []; $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ @@ -80,7 +80,7 @@ public function click(WebDriverCoordinates $where = null) */ public function contextClick(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $moveAction = $where ? [$this->createMoveAction($where)] : []; $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ @@ -122,7 +122,7 @@ public function contextClick(WebDriverCoordinates $where = null) */ public function doubleClick(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $clickActions = $this->createClickActions(); $moveAction = null === $where ? [] : [$this->createMoveAction($where)]; $this->executor->execute(DriverCommand::ACTIONS, [ @@ -152,7 +152,7 @@ public function doubleClick(WebDriverCoordinates $where = null) */ public function mouseDown(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ [ @@ -192,7 +192,7 @@ public function mouseMove( $x_offset = null, $y_offset = null ) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::ACTIONS, [ 'actions' => [ [ @@ -230,7 +230,7 @@ public function mouseMove( */ public function mouseUp(WebDriverCoordinates $where = null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $moveAction = $where ? [$this->createMoveAction($where)] : []; $this->executor->execute(DriverCommand::ACTIONS, [ diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 55a879c0d..8bd89ef44 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -112,8 +112,8 @@ public function activeElement() $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); - $w3cCompliant = $this->driver instanceof RemoteWebDriver ? $this->driver->isW3cCompliant() : false; + $isW3cCompliant = ($this->driver instanceof RemoteWebDriver) ? $this->driver->isW3cCompliant() : false; - return new RemoteWebElement($method, JsonWireCompat::getElement($response), $w3cCompliant); + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $isW3cCompliant); } } diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 6d3a7d833..4d3573c0c 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -62,23 +62,23 @@ class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInpu /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; /** * @param HttpCommandExecutor $commandExecutor * @param string $sessionId * @param WebDriverCapabilities|null $capabilities - * @param bool $w3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec + * @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec */ protected function __construct( HttpCommandExecutor $commandExecutor, $sessionId, WebDriverCapabilities $capabilities = null, - $w3cCompliant = false + $isW3cCompliant = false ) { $this->executor = $commandExecutor; $this->sessionID = $sessionId; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; if ($capabilities !== null) { $this->capabilities = $capabilities; @@ -168,7 +168,7 @@ public static function create( $value = $response->getValue(); if (!$w3c_compliant = isset($value['capabilities'])) { - $executor->disableW3CCompliance(); + $executor->disableW3cCompliance(); } $returnedCapabilities = new DesiredCapabilities($w3c_compliant ? $value['capabilities'] : $value); @@ -232,7 +232,7 @@ public function findElement(WebDriverBy $by) { $raw_element = $this->execute( DriverCommand::FIND_ELEMENT, - JsonWireCompat::getUsing($by, $this->w3cCompliant) + JsonWireCompat::getUsing($by, $this->isW3cCompliant) ); return $this->newElement(JsonWireCompat::getElement($raw_element)); @@ -249,7 +249,7 @@ public function findElements(WebDriverBy $by) { $raw_elements = $this->execute( DriverCommand::FIND_ELEMENTS, - JsonWireCompat::getUsing($by, $this->w3cCompliant) + JsonWireCompat::getUsing($by, $this->isW3cCompliant) ); $elements = []; @@ -428,7 +428,7 @@ public function wait($timeout_in_second = 30, $interval_in_millisecond = 250) */ public function manage() { - return new WebDriverOptions($this->getExecuteMethod(), $this->w3cCompliant); + return new WebDriverOptions($this->getExecuteMethod(), $this->isW3cCompliant); } /** @@ -459,7 +459,7 @@ public function switchTo() public function getMouse() { if (!$this->mouse) { - $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->w3cCompliant); + $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->isW3cCompliant); } return $this->mouse; @@ -605,7 +605,7 @@ public function execute($command_name, $params = []) */ public function isW3cCompliant() { - return $this->w3cCompliant; + return $this->isW3cCompliant; } /** @@ -620,10 +620,12 @@ protected function prepareScriptArguments(array $arguments) foreach ($arguments as $key => $value) { if ($value instanceof WebDriverElement) { $args[$key] = [ - $this->w3cCompliant ? JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER : 'ELEMENT' => $value->getID(), + $this->isW3cCompliant ? + JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER + : 'ELEMENT' => $value->getID(), ]; } else { - if (\is_array($value)) { + if (is_array($value)) { $value = $this->prepareScriptArguments($value); } $args[$key] = $value; @@ -653,7 +655,7 @@ protected function getExecuteMethod() */ protected function newElement($id) { - return new RemoteWebElement($this->getExecuteMethod(), $id, $this->w3cCompliant); + return new RemoteWebElement($this->getExecuteMethod(), $id, $this->isW3cCompliant); } /** @@ -669,7 +671,7 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return new DesiredCapabilities(); } - if (\is_array($desired_capabilities)) { + if (is_array($desired_capabilities)) { return new DesiredCapabilities($desired_capabilities); } diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php index b07b18227..75dfa3ba2 100644 --- a/lib/Remote/RemoteWebElement.php +++ b/lib/Remote/RemoteWebElement.php @@ -46,19 +46,19 @@ class RemoteWebElement implements WebDriverElement, WebDriverLocatable /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; /** * @param RemoteExecuteMethod $executor * @param string $id - * @param bool $w3cCompliant + * @param bool $isW3cCompliant */ - public function __construct(RemoteExecuteMethod $executor, $id, $w3cCompliant = false) + public function __construct(RemoteExecuteMethod $executor, $id, $isW3cCompliant = false) { $this->executor = $executor; $this->id = $id; $this->fileDetector = new UselessFileDetector(); - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -73,20 +73,22 @@ public function clear() [':id' => $this->id] ); - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::ACTIONS, [ - 'actions' => [[ - 'type' => 'key', - 'id' => 'keyboard', - 'actions' => [ - ['type' => 'keyDown' , 'value' => WebDriverKeys::CONTROL], - ['type' => 'keyDown' , 'value' => 'a'], - ['type' => 'keyUp' , 'value' => WebDriverKeys::CONTROL], - ['type' => 'keyUp' , 'value' => 'a'], - ['type' => 'keyDown' , 'value' => WebDriverKeys::BACKSPACE], - ['type' => 'keyUp' , 'value' => WebDriverKeys::BACKSPACE], + 'actions' => [ + [ + 'type' => 'key', + 'id' => 'keyboard', + 'actions' => [ + ['type' => 'keyDown', 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyDown', 'value' => 'a'], + ['type' => 'keyUp', 'value' => WebDriverKeys::CONTROL], + ['type' => 'keyUp', 'value' => 'a'], + ['type' => 'keyDown', 'value' => WebDriverKeys::BACKSPACE], + ['type' => 'keyUp', 'value' => WebDriverKeys::BACKSPACE], + ], ], - ]], + ], ]); } @@ -118,7 +120,7 @@ public function click() */ public function findElement(WebDriverBy $by) { - $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); $params[':id'] = $this->id; $raw_element = $this->executor->execute( @@ -139,7 +141,7 @@ public function findElement(WebDriverBy $by) */ public function findElements(WebDriverBy $by) { - $params = JsonWireCompat::getUsing($by, $this->w3cCompliant); + $params = JsonWireCompat::getUsing($by, $this->isW3cCompliant); $params[':id'] = $this->id; $raw_elements = $this->executor->execute( DriverCommand::FIND_CHILD_ELEMENTS, @@ -167,7 +169,7 @@ public function getAttribute($attribute_name) ':id' => $this->id, ]; - if ($this->w3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { + if ($this->isW3cCompliant && ($attribute_name === 'value' || $attribute_name === 'index')) { $value = $this->executor->execute(DriverCommand::GET_ELEMENT_PROPERTY, $params); if ($value === true) { @@ -357,7 +359,7 @@ public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); if ($local_file === null) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $params = [ 'text' => (string) $value, ':id' => $this->id, @@ -374,7 +376,7 @@ public function sendKeys($value) return $this; } - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $params = [ 'text' => $local_file, ':id' => $this->id, @@ -420,7 +422,7 @@ public function setFileDetector(FileDetector $detector) */ public function submit() { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute(DriverCommand::EXECUTE_SCRIPT, [ // cannot call the submit method directly in case an input of this form is named "submit" 'script' => sprintf( @@ -459,7 +461,7 @@ public function getID() */ public function equals(WebDriverElement $other) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { throw new UnsupportedOperationException('"elementEquals" is not supported by the W3C specification'); } @@ -478,7 +480,7 @@ public function equals(WebDriverElement $other) */ protected function newElement($id) { - return new static($this->executor, $id, $this->w3cCompliant); + return new static($this->executor, $id, $this->isW3cCompliant); } /** diff --git a/lib/WebDriverOptions.php b/lib/WebDriverOptions.php index 4c584bd07..682918a17 100644 --- a/lib/WebDriverOptions.php +++ b/lib/WebDriverOptions.php @@ -31,12 +31,12 @@ class WebDriverOptions /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; - public function __construct(ExecuteMethod $executor, $w3cCompliant = false) + public function __construct(ExecuteMethod $executor, $isW3cCompliant = false) { $this->executor = $executor; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -133,7 +133,7 @@ public function getCookies() */ public function timeouts() { - return new WebDriverTimeouts($this->executor, $this->w3cCompliant); + return new WebDriverTimeouts($this->executor, $this->isW3cCompliant); } /** diff --git a/lib/WebDriverTimeouts.php b/lib/WebDriverTimeouts.php index eeba9a10e..5735cb48d 100644 --- a/lib/WebDriverTimeouts.php +++ b/lib/WebDriverTimeouts.php @@ -30,12 +30,12 @@ class WebDriverTimeouts /** * @var bool */ - protected $w3cCompliant; + protected $isW3cCompliant; - public function __construct(ExecuteMethod $executor, $w3cCompliant = false) + public function __construct(ExecuteMethod $executor, $isW3cCompliant = false) { $this->executor = $executor; - $this->w3cCompliant = $w3cCompliant; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -46,7 +46,7 @@ public function __construct(ExecuteMethod $executor, $w3cCompliant = false) */ public function implicitlyWait($seconds) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute( DriverCommand::IMPLICITLY_WAIT, ['implicit' => $seconds * 1000] @@ -71,7 +71,7 @@ public function implicitlyWait($seconds) */ public function setScriptTimeout($seconds) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['script' => $seconds * 1000] @@ -96,7 +96,7 @@ public function setScriptTimeout($seconds) */ public function pageLoadTimeout($seconds) { - if ($this->w3cCompliant) { + if ($this->isW3cCompliant) { $this->executor->execute( DriverCommand::SET_SCRIPT_TIMEOUT, ['pageLoad' => $seconds * 1000] From bd34170cb3c17322dbc785fede30b6c88ffd651c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 28 Oct 2019 18:21:02 +0100 Subject: [PATCH 032/373] Compatibility with Symfony 5 --- CHANGELOG.md | 1 + composer.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e733516..e2662351d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ### Changed - Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). +- Allow installation of Symfony 5 components. ## 1.7.1 - 2019-06-13 ### Fixed diff --git a/composer.json b/composer.json index 2a9929725..3a841a504 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "minimum-stability": "beta", "require": { "php": "^5.6 || ~7.0", - "symfony/process": "^2.8 || ^3.1 || ^4.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0", "ext-curl": "*", "ext-zip": "*", "ext-mbstring": "*", @@ -26,7 +26,7 @@ "squizlabs/php_codesniffer": "^2.6", "php-mock/php-mock-phpunit": "^1.1", "php-coveralls/php-coveralls": "^2.0", - "symfony/var-dumper": "^3.3 || ^4.0", + "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0", "jakub-onderka/php-parallel-lint": "^0.9.2" }, "suggest": { From e9277912d553971c2b73bf19df3fb44323f1f8b7 Mon Sep 17 00:00:00 2001 From: Mohammadreza Yektamaram Date: Tue, 1 Oct 2019 14:51:24 +0330 Subject: [PATCH 033/373] Minor grammar fixes --- README.md | 2 +- example.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b25bee453..0fa9db31c 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ We have a great community willing to help you! - **Via our Facebook Group** - If you have questions or are an active contributor consider joining our [facebook group](https://www.facebook.com/groups/phpwebdriver/) and contribute to communal discussion and support - **Via StackOverflow** - You can also [ask a question](https://stackoverflow.com/questions/ask?tags=php+selenium-webdriver) or find many already answered question on StackOverflow -- **Via GitHub** - Another option if you have a question (or bug report) is to [submit it here](https://github.com/facebook/php-webdriver/issues/new) as an new issue +- **Via GitHub** - Another option if you have a question (or bug report) is to [submit it here](https://github.com/facebook/php-webdriver/issues/new) as a new issue ## Contributing diff --git a/example.php b/example.php index f1109ed54..4dbb93900 100644 --- a/example.php +++ b/example.php @@ -23,7 +23,7 @@ require_once('vendor/autoload.php'); -// start Chrome with 5 second timeout +// start Chrome with 5 seconds timeout $host = 'http://localhost:4444/wd/hub'; // this is the default $capabilities = DesiredCapabilities::chrome(); $driver = RemoteWebDriver::create($host, $capabilities, 5000); From a21b3c82dabb8a777e81aed5a8239cba67efc115 Mon Sep 17 00:00:00 2001 From: "Benjamin R. White" Date: Tue, 27 Feb 2018 17:21:16 +0000 Subject: [PATCH 034/373] Allow combined not() and presenceOfElementLocated() functionality --- CHANGELOG.md | 3 +++ lib/WebDriverExpectedCondition.php | 9 ++++--- tests/unit/WebDriverExpectedConditionTest.php | 24 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2662351d..bbd2f4d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). - Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). - Allow installation of Symfony 5 components. +### Fixed +- `WebDriverExpectedCondition::presenceOfElementLocated()` works correctly when used within `WebDriverExpectedCondition::not()`. + ## 1.7.1 - 2019-06-13 ### Fixed - Error `Call to a member function toArray()` if capabilities were already converted to an array. diff --git a/lib/WebDriverExpectedCondition.php b/lib/WebDriverExpectedCondition.php index 15c7b3d2d..4bab4707b 100644 --- a/lib/WebDriverExpectedCondition.php +++ b/lib/WebDriverExpectedCondition.php @@ -148,7 +148,11 @@ public static function presenceOfElementLocated(WebDriverBy $by) { return new static( function (WebDriver $driver) use ($by) { - return $driver->findElement($by); + try { + return $driver->findElement($by); + } catch (NoSuchElementException $e) { + return false; + } } ); } @@ -423,8 +427,7 @@ function (WebDriver $driver) use ($by, $text) { */ public static function elementToBeClickable(WebDriverBy $by) { - $visibility_of_element_located = - self::visibilityOfElementLocated($by); + $visibility_of_element_located = self::visibilityOfElementLocated($by); return new static( function (WebDriver $driver) use ($visibility_of_element_located) { diff --git a/tests/unit/WebDriverExpectedConditionTest.php b/tests/unit/WebDriverExpectedConditionTest.php index 8cd4203a9..6dde484ef 100644 --- a/tests/unit/WebDriverExpectedConditionTest.php +++ b/tests/unit/WebDriverExpectedConditionTest.php @@ -135,6 +135,28 @@ public function testShouldDetectPresenceOfElementLocatedCondition() $this->assertSame($element, $this->wait->until($condition)); } + public function testShouldDetectNotPresenceOfElementLocatedCondition() + { + $element = new RemoteWebElement(new RemoteExecuteMethod($this->driverMock), 'id'); + + $this->driverMock->expects($this->at(0)) + ->method('findElement') + ->with($this->isInstanceOf(WebDriverBy::class)) + ->willReturn($element); + + $this->driverMock->expects($this->at(1)) + ->method('findElement') + ->with($this->isInstanceOf(WebDriverBy::class)) + ->willThrowException(new NoSuchElementException('')); + + $condition = WebDriverExpectedCondition::not( + WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::cssSelector('.foo')) + ); + + $this->assertFalse(call_user_func($condition->getApply(), $this->driverMock)); + $this->assertTrue(call_user_func($condition->getApply(), $this->driverMock)); + } + public function testShouldDetectPresenceOfAllElementsLocatedByCondition() { $element = $this->createMock(RemoteWebElement::class); @@ -160,7 +182,7 @@ public function testShouldDetectVisibilityOfElementLocatedCondition() // Call #1: throws NoSuchElementException // Call #2: return Element, but isDisplayed will throw StaleElementReferenceException // Call #3: return Element, but isDisplayed will return false - // Call #4: return Element, isDisplayed will true and condition will match + // Call #4: return Element, isDisplayed will return true and condition will match $element = $this->createMock(RemoteWebElement::class); $element->expects($this->at(0)) From 1f8d242f98310b12d8dbfc39859aad6b13f35708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 1 Oct 2018 12:16:55 +0200 Subject: [PATCH 035/373] Use polyfill-mbstring when ext-mbstring isn't available --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3a841a504..e5aef5a03 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ "require": { "php": "^5.6 || ~7.0", "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0", + "symfony/polyfill-mbstring": "^1.12", "ext-curl": "*", "ext-zip": "*", - "ext-mbstring": "*", "ext-json": "*" }, "require-dev": { From dc50f3f0fd5506230f4fcc48670bf1edcf1d65ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 00:04:33 +0100 Subject: [PATCH 036/373] Skipped tests must be completely excluded from Saucelabs to avoid starting the driver there, otherwise the build is not marked passed on Saucelabs dashboard --- tests/functional/RemoteWebDriverFindElementTest.php | 3 +++ tests/functional/WebDriverActionsTest.php | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index 066179f1f..2eb3e38c7 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -62,6 +62,9 @@ public function testShouldFindMultipleElements() $this->assertContainsOnlyInstancesOf(RemoteWebElement::class, $elements); } + /** + * @group exclude-saucelabs + */ public function testEscapeCssSelector() { if (getenv('GECKODRIVER') !== '1') { diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 1f03c8899..07af88085 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -88,6 +88,7 @@ public function testShouldClickAndHoldOnElementAndRelease() } /** + * @group exclude-saucelabs * @covers ::__construct * @covers ::contextClick * @covers ::perform From a29494a551b8efb8afef51dc4f7af2cb7af98d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sun, 10 Nov 2019 23:01:03 +0100 Subject: [PATCH 037/373] Add Chromedriver Travis build (without Selenium server proxy) --- .travis.yml | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 950e8fe64..986df3e31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,8 @@ env: matrix: include: # Codestyle check build - - php: '7.3' + - name: 'Code style and static analysis' + php: '7.3' env: CHECK_CODESTYLE=1 before_install: - phpenv config-rm xdebug.ini @@ -30,18 +31,19 @@ matrix: after_success: ~ # Build with lowest possible dependencies on lowest possible PHP - - php: '5.6' + - name: 'Lowest dependencies build' + php: '5.6' env: DEPENDENCIES="--prefer-lowest" # Firefox inside Travis environment - - name: 'Firefox 45 on Travis (OSS protocol)' + - name: 'Firefox 45 on Travis (OSS protocol); via Selenium server' 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)' + - name: 'Firefox latest on Travis (W3C protocol); no Selenium server' php: 7.3 env: - BROWSER_NAME="firefox" @@ -49,9 +51,22 @@ matrix: addons: firefox: latest - # Stable Chrome + Chromedriver inside Travis environment - - php: '7.3' - env: BROWSER_NAME="chrome" CHROME_HEADLESS="1" + # Stable Chrome + Chromedriver inside Travis environment via Selenium server proxy + - name: 'Chrome stable on Travis; via Selenium server' + php: '7.3' + env: + - BROWSER_NAME="chrome" + - CHROME_HEADLESS="1" + addons: + chrome: stable + + # Stable Chrome + Chromedriver inside Travis environment directly via Chromedriver + - name: 'Chrome stable on Travis; no Selenium server' + php: '7.3' + env: + - BROWSER_NAME="chrome" + - CHROME_HEADLESS="1" + - CHROMEDRIVER="1" addons: chrome: stable @@ -113,6 +128,8 @@ before_script: - 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 - if [ "$GECKODRIVER" = "1" ]; then geckodriver/geckodriver &> ./logs/geckodriver.log & + elif [ "$CHROMEDRIVER" = "1" ]; then + chromedriver/chromedriver --port=4444 --url-base=/wd/hub &> ./logs/chromedriver.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 @@ -130,6 +147,7 @@ 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 + - if [ -f ./logs/chromedriver.log ]; then cat ./logs/chromedriver.log; fi after_success: - travis_retry php vendor/bin/php-coveralls -v From 61ab881143846201c314bc9f9988a8831a75f26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sun, 10 Nov 2019 23:51:18 +0100 Subject: [PATCH 038/373] Use newer Selenium server where possible But we can easily update only to 3.14.0 (latest version which includes HtmlUnit). However update to 3.14.159 should be done soon (but HtmlUnit needs to be sideloaded). --- .travis.yml | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 986df3e31..fc7368965 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ env: global: - DISPLAY=:99.0 - BROWSER_NAME="htmlunit" + - SELENIUM_SERVER="https://selenium-release.storage.googleapis.com/3.14/selenium-server-standalone-3.14.0.jar" # Latest version including HtmlUnit matrix: include: @@ -36,9 +37,11 @@ matrix: env: DEPENDENCIES="--prefer-lowest" # Firefox inside Travis environment - - name: 'Firefox 45 on Travis (OSS protocol); via Selenium server' + - name: 'Firefox 45 on Travis (OSS protocol); via legacy Selenium server' php: '7.3' - env: BROWSER_NAME="firefox" + env: + - BROWSER_NAME="firefox" + - SELENIUM_SERVER="legacy" addons: firefox: "45.8.0esr" @@ -114,7 +117,6 @@ matrix: cache: directories: - $HOME/.composer/cache - - jar install: - travis_retry composer self-update @@ -123,15 +125,24 @@ 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 + - if [ "$GECKODRIVER" = "1" ]; then mkdir -p 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 # TODO: start only when needed (ie. not in headless mode) + - if [ ! -f jar/selenium-server-standalone.jar ] && [ -n "$SELENIUM_SERVER" ]; then + mkdir -p jar; + if [ "$SELENIUM_SERVER" = "legacy" ]; then + wget -q -t 3 -O jar/selenium-server-standalone.jar https://selenium-release.storage.googleapis.com/3.8/selenium-server-standalone-3.8.1.jar; + else + wget -q -t 3 -O jar/selenium-server-standalone.jar $SELENIUM_SERVER; + fi + fi - if [ "$GECKODRIVER" = "1" ]; then geckodriver/geckodriver &> ./logs/geckodriver.log & elif [ "$CHROMEDRIVER" = "1" ]; then chromedriver/chromedriver --port=4444 --url-base=/wd/hub &> ./logs/chromedriver.log & + elif [ "$SELENIUM_SERVER" = "legacy" ]; then + java -Dwebdriver.firefox.marionette=false -Dwebdriver.chrome.driver="$PWD/chromedriver/chromedriver" -jar jar/selenium-server-standalone.jar -enablePassThrough false -log ./logs/selenium.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 & + java -Dwebdriver.chrome.driver="$PWD/chromedriver/chromedriver" -Dwebdriver.gecko.driver="$PWD/geckodriver/geckodriver" -jar jar/selenium-server-standalone.jar -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 & From a4b0d7130ed35c2083f62ae0bb135c563df68364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 10:21:11 +0100 Subject: [PATCH 039/373] Convert W3C invalid capabilities to corresponding ones This avoids `Facebook\WebDriver\Exception\UnknownServerException: Illegal key values seen in w3c capabilities: [chromeOptions]` error with new Selenium server. This conversion is necessary to maintain BC - simply renaming chromeOptions to goog:chromeOptions will break capabilities with Chromedriver <2.31. --- lib/Chrome/ChromeOptions.php | 7 ++++++- lib/Remote/RemoteWebDriver.php | 32 ++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/Chrome/ChromeOptions.php b/lib/Chrome/ChromeOptions.php index 901dc5388..949d6708b 100644 --- a/lib/Chrome/ChromeOptions.php +++ b/lib/Chrome/ChromeOptions.php @@ -25,9 +25,14 @@ class ChromeOptions { /** - * The key of chrome options in desired capabilities. + * The key of chrome options desired capabilities (in legacy OSS JsonWire protocol) + * @deprecated */ const CAPABILITY = 'chromeOptions'; + /** + * The key of chrome options desired capabilities (in W3C compatible protocol) + */ + const CAPABILITY_W3C = 'goog:chromeOptions'; /** * @var array */ diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 4d3573c0c..c87a572f9 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -140,15 +140,15 @@ public static function create( // W3C $parameters = [ 'capabilities' => [ - 'firstMatch' => [$desired_capabilities->toArray()], + 'firstMatch' => [static::convertCapabilitiesToW3c($desired_capabilities->toArray())], ], ]; - // Legacy protocol - if (null !== $required_capabilities && $required_capabilities_array = $required_capabilities->toArray()) { - $parameters['capabilities']['alwaysMatch'] = $required_capabilities_array; + if ($required_capabilities !== null && $required_capabilities_array = $required_capabilities->toArray()) { + $parameters['capabilities']['alwaysMatch'] = static::convertCapabilitiesToW3c($required_capabilities_array); } + // Legacy protocol if ($required_capabilities !== null) { // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities. // This has changed with the W3C WebDriver spec, but is the only way how to pass these @@ -677,4 +677,28 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return $desired_capabilities; } + + /** + * Convert keys invalid in W3C capabilities to corresponding ones for W3C. + * + * @param array $capabilitiesArray + * @return array + */ + protected static function convertCapabilitiesToW3c(array $capabilitiesArray) + { + if (array_key_exists(ChromeOptions::CAPABILITY, $capabilitiesArray)) { + if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $capabilitiesArray)) { + $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = array_merge( + $capabilitiesArray[ChromeOptions::CAPABILITY], + $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] + ); + } else { + $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = $capabilitiesArray[ChromeOptions::CAPABILITY]; + } + + unset($capabilitiesArray[ChromeOptions::CAPABILITY]); + } + + return $capabilitiesArray; + } } From 3597a6bcaddb2a462d71cf81620346892991ad47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 10:37:25 +0100 Subject: [PATCH 040/373] =?UTF-8?q?Enable=20W3C=20protocol=20for=20Chromed?= =?UTF-8?q?river=20=F0=9F=8E=89=20#469?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now, W3C was forcily disabled for Chromedriver using Capabilities on startup. As we now experimentaly support W3C protocol, this is no longer needed. If W3C is detected during initial handshake, the browser will be started using W3C dialect (otherwise it will use legacy OSS JsonWire protocol). --- lib/Remote/RemoteWebDriver.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index c87a572f9..d2e302f44 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -112,23 +112,6 @@ public static function create( $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities); - // Hotfix: W3C WebDriver protocol is not yet supported by php-webdriver, so we must force Chromedriver to - // not use the W3C protocol by default (which is what Chromedriver does starting with version 75). - if ($desired_capabilities->getBrowserName() === WebDriverBrowserType::CHROME - && mb_strpos($selenium_server_url, 'browserstack') === false // see https://github.com/facebook/php-webdriver/issues/644 - ) { - $currentChromeOptions = $desired_capabilities->getCapability(ChromeOptions::CAPABILITY); - $chromeOptions = !empty($currentChromeOptions) ? $currentChromeOptions : new ChromeOptions(); - - if ($chromeOptions instanceof ChromeOptions && !isset($chromeOptions->toArray()['w3c'])) { - $chromeOptions->setExperimentalOption('w3c', false); - } elseif (is_array($chromeOptions) && !isset($chromeOptions['w3c'])) { - $chromeOptions['w3c'] = false; - } - - $desired_capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); - } - $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port); if ($connection_timeout_in_ms !== null) { $executor->setConnectionTimeout($connection_timeout_in_ms); From a9685bf2aa22de07d97ca93ff452e5016ee3141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 12:41:45 +0100 Subject: [PATCH 041/373] Add JsonWire travis build on Chromedriver to make sure we won't break the old protocol --- .travis.yml | 19 +++++++++++++++---- tests/functional/WebDriverTestCase.php | 5 +++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc7368965..4a8a268a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,8 +54,8 @@ matrix: addons: firefox: latest - # Stable Chrome + Chromedriver inside Travis environment via Selenium server proxy - - name: 'Chrome stable on Travis; via Selenium server' + # Stable Chrome + Chromedriver (W3C mode) inside Travis environment via Selenium server proxy + - name: 'Chrome stable on Travis (W3C protocol); via Selenium server' php: '7.3' env: - BROWSER_NAME="chrome" @@ -63,8 +63,8 @@ matrix: addons: chrome: stable - # Stable Chrome + Chromedriver inside Travis environment directly via Chromedriver - - name: 'Chrome stable on Travis; no Selenium server' + # Stable Chrome + Chromedriver (W3C mode) inside Travis environment directly via Chromedriver + - name: 'Chrome stable on Travis (W3C protocol); no Selenium server' php: '7.3' env: - BROWSER_NAME="chrome" @@ -73,6 +73,17 @@ matrix: addons: chrome: stable + # Stable Chrome + Chromedriver (JsonWire OSS mode) inside Travis environment directly via Chromedriver + - name: 'Chrome stable on Travis (OSS protocol); no Selenium server' + php: '7.3' + env: + - BROWSER_NAME="chrome" + - CHROME_HEADLESS="1" + - CHROMEDRIVER="1" + - DISABLE_W3C_PROTOCOL="1" + addons: + chrome: stable + # Saucelabs builds - php: '7.3' env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 7a0f8af23..920eb86a7 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -57,6 +57,11 @@ protected function setUp() $chromeOptions = new ChromeOptions(); // --no-sandbox is a workaround for Chrome crashing: https://github.com/SeleniumHQ/selenium/issues/4961 $chromeOptions->addArguments(['--headless', 'window-size=1024,768', '--no-sandbox']); + + if (getenv('DISABLE_W3C_PROTOCOL')) { + $chromeOptions->setExperimentalOption('w3c', false); + } + $this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); } elseif (getenv('GECKODRIVER') === '1') { $this->serverUrl = 'http://localhost:4444/'; From 041658be9e1284578f5fba1198b10dc8922ceb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 13:25:45 +0100 Subject: [PATCH 042/373] Improve W3C builds detection in conditionally-run tests --- .../RemoteWebDriverFindElementTest.php | 8 +++--- tests/functional/RemoteWebDriverTest.php | 1 - tests/functional/RemoteWebElementTest.php | 4 +-- tests/functional/WebDriverActionsTest.php | 4 +-- tests/functional/WebDriverTestCase.php | 26 +++++++++++++++++++ 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/functional/RemoteWebDriverFindElementTest.php b/tests/functional/RemoteWebDriverFindElementTest.php index 2eb3e38c7..c5671b497 100644 --- a/tests/functional/RemoteWebDriverFindElementTest.php +++ b/tests/functional/RemoteWebDriverFindElementTest.php @@ -67,11 +67,9 @@ public function testShouldFindMultipleElements() */ public function testEscapeCssSelector() { - if (getenv('GECKODRIVER') !== '1') { - $this->markTestSkipped( - 'CSS selectors containing special characters are not supported by the legacy protocol' - ); - } + self::skipForJsonWireProtocol( + 'CSS selectors containing special characters are not supported by the legacy protocol' + ); $this->driver->get($this->getTestPageUrl('escape_css.html')); diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index 9105aa93f..72fc30505 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -150,7 +150,6 @@ public function testShouldCloseWindow() $this->driver->get($this->getTestPageUrl('open_new_window.html')); $this->driver->findElement(WebDriverBy::cssSelector('a'))->click(); - // Mandatory for Geckodriver $this->driver->wait()->until(WebDriverExpectedCondition::numberOfWindowsToBe(2)); $this->assertCount(2, $this->driver->getWindowHandles()); diff --git a/tests/functional/RemoteWebElementTest.php b/tests/functional/RemoteWebElementTest.php index a4684ace3..52877dcda 100644 --- a/tests/functional/RemoteWebElementTest.php +++ b/tests/functional/RemoteWebElementTest.php @@ -272,9 +272,7 @@ public function testShouldSubmitFormByClickOnSubmitInput() */ public function testShouldCompareEqualsElement() { - if (getenv('GECKODRIVER') === '1') { - $this->markTestSkipped('"equals" is not supported by the W3C specification'); - } + self::skipForW3cProtocol('"equals" is not supported by the W3C specification'); $this->driver->get($this->getTestPageUrl('index.html')); diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 07af88085..9798f6e5a 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -49,7 +49,7 @@ public function testShouldClickOnElement() $logs = ['mouseover item-1', 'mousedown item-1', 'mouseup item-1', 'click item-1']; $loggedEvents = $this->retrieveLoggedEvents(); - if ('1' === getenv('GECKODRIVER')) { + if (getenv('GECKODRIVER') === '1') { $loggedEvents = array_slice($loggedEvents, 0, count($logs)); // Firefox sometimes triggers some extra events // it's not related to Geckodriver, it's Firefox's own behavior @@ -77,7 +77,7 @@ public function testShouldClickAndHoldOnElementAndRelease() ->release() ->perform(); - if ('1' === getenv('GECKODRIVER')) { + if (self::isW3cProtocolBuild()) { $this->assertArraySubset(['mouseover item-1', 'mousedown item-1'], $this->retrieveLoggedEvents()); } else { $this->assertSame( diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 920eb86a7..8b35a7700 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -102,6 +102,32 @@ public static function isSauceLabsBuild() return getenv('SAUCELABS') ? true : false; } + /** + * @return bool + */ + public static function isW3cProtocolBuild() + { + return getenv('GECKODRIVER') === '1' + || (getenv('BROWSER_NAME') === 'chrome' + && getenv('DISABLE_W3C_PROTOCOL') !== '1' + && !self::isSauceLabsBuild()); + } + + public static function skipForW3cProtocol($message = 'Not supported by W3C specification') + { + if (static::isW3cProtocolBuild()) { + static::markTestSkipped($message); + } + } + + public static function skipForJsonWireProtocol($message = 'Not supported by JsonWire protocol') + { + if (getenv('GECKODRIVER') !== '1' + && (getenv('CHROMEDRIVER') !== '1' || getenv('DISABLE_W3C_PROTOCOL') === '1')) { + static::markTestSkipped($message); + } + } + /** * Get the URL of given test HTML on running webserver. * From d249dea3f0da638c2d44c028b4a5d9247cc2fa87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 11 Nov 2019 16:36:05 +0100 Subject: [PATCH 043/373] Update changelog and readme to let the world know about W3C protocol experimental support --- CHANGELOG.md | 3 +++ README.md | 38 ++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbd2f4d52..b83a21743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ This project versioning adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Added +- Experimental W3C WebDriver protocol support. The protocol will be used automatically when remote end (like Geckodriver, newer Chromedriver etc.) supports it. + ### Changed - Revert no longer needed workaround for Chromedriver bug [2943](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2943). - Allow installation of Symfony 5 components. diff --git a/README.md b/README.md index 0fa9db31c..71742583b 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,16 @@ ## Description Php-webdriver library is PHP language binding for Selenium WebDriver, which allows you to control web browsers from PHP. -This library is compatible with Selenium server version 2.x and 3.x. -It implements the [JsonWireProtocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol), which is currently supported -by the Selenium server and will also implement the [W3C WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) specification in the future. +This library is compatible with Selenium server version 2.x, 3.x and 4.x. + +The library supports [JsonWireProtocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol) and also +implements **experimental support** of [W3C WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html). +The W3C WebDriver support is not yet full-featured, however it should allow to control Firefox via Geckodriver and new +versions of Chrome and Chromedriver with just a slight limitations. The concepts of this library are very similar to the "official" Java, .NET, Python and Ruby bindings from the [Selenium project](https://github.com/SeleniumHQ/selenium/). -**As of 2013, this PHP client has been rewritten from scratch.** -Using the old version? Check out [Adam Goucher's fork](https://github.com/Element-34/php-webdriver) of it. - Looking for API documentation of php-webdriver? See [https://facebook.github.io/php-webdriver/](https://facebook.github.io/php-webdriver/latest/) Any complaints, questions, or ideas? Post them in the user group https://www.facebook.com/groups/phpwebdriver/. @@ -41,24 +41,25 @@ Then install the library: The required server is the `selenium-server-standalone-#.jar` file provided here: http://selenium-release.storage.googleapis.com/index.html -Download and run the server by replacing # with the current server version. Keep in mind **you must have Java 8+ installed to run this command**. +Download and run the server by **replacing #** with the current server version. Keep in mind **you must have Java 8+ installed to run this command**. java -jar selenium-server-standalone-#.jar -**NOTE:** If using Firefox, see alternate command below. - ### Create a Browser Session When creating a browser session, be sure to pass the url of your running server. ```php // This would be the url of the host running the server-standalone.jar -$host = 'http://localhost:4444/wd/hub'; // this is the default +$host = 'http://localhost:4444/wd/hub'; // this is the default url and port where Selenium server starts ``` ##### Launch Chrome -Make sure to have latest Chrome and [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) versions installed. +Install latest Chrome and [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads). + +The `chromedriver` binary must be placed in system `PATH` directory, otherwise you must provide the path when starting Selenium server +(eg. `java -Dwebdriver.chrome.driver="/path/to/chromedriver" -jar selenium-server-standalone-#.jar`). ```php $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome()); @@ -66,14 +67,11 @@ $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome()); ##### Launch Firefox -Make sure to have latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases) installed. - -Because Firefox (and Geckodriver) only support the new W3C WebDriver protocol (which is yet to be implemented by php-webdriver - see [issue #469](https://github.com/facebook/php-webdriver/issues/469)), -the protocols must be translated by Selenium Server - this feature is *partially* available in Selenium Server versions 3.5.0-3.8.1 and you can enable it like this: +Install latest Firefox and [Geckodriver](https://github.com/mozilla/geckodriver/releases). - java -jar selenium-server-standalone-3.8.1.jar -enablePassThrough false +The `geckodriver` binary must be placed in system `PATH` directory, otherwise you must provide the path when starting Selenium server +(eg. `java -Dwebdriver.gecko.driver="/path/to/geckodriver" -jar selenium-server-standalone-#.jar`). -Now you can start Firefox from your code: ```php $driver = RemoteWebDriver::create($host, DesiredCapabilities::firefox()); @@ -82,9 +80,9 @@ $driver = RemoteWebDriver::create($host, DesiredCapabilities::firefox()); ### Customize Desired Capabilities ```php -$desired_capabilities = DesiredCapabilities::firefox(); -$desired_capabilities->setCapability('acceptSslCerts', false); -$driver = RemoteWebDriver::create($host, $desired_capabilities); +$desiredCapabilities = DesiredCapabilities::firefox(); +$desiredCapabilities->setCapability('acceptSslCerts', false); +$driver = RemoteWebDriver::create($host, $desiredCapabilities); ``` * See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities for more details. From 42f82dd233ea52f64f36f143a57c6d6df89c22c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 12 Nov 2019 23:54:22 +0100 Subject: [PATCH 044/373] Define branch alias for version 1.8 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e5aef5a03..a6aec98ae 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ }, "extra": { "branch-alias": { - "dev-community": "1.5-dev" + "dev-community": "1.8.x-dev" } } } From a6cad80b88fcb3c3823afe85d653d0c46c05944c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Thu, 14 Nov 2019 19:10:16 +0100 Subject: [PATCH 045/373] Add convertor to W3C compatible capabilities (fixes #676, part of #469) --- lib/Remote/DesiredCapabilities.php | 73 ++++++++++ lib/Remote/RemoteWebDriver.php | 31 +--- tests/unit/Remote/DesiredCapabilitiesTest.php | 137 ++++++++++++++++++ 3 files changed, 213 insertions(+), 28 deletions(-) diff --git a/lib/Remote/DesiredCapabilities.php b/lib/Remote/DesiredCapabilities.php index 533c8873a..c78d2302b 100644 --- a/lib/Remote/DesiredCapabilities.php +++ b/lib/Remote/DesiredCapabilities.php @@ -178,6 +178,79 @@ public function toArray() return $this->capabilities; } + /** + * @return array + */ + public function toW3cCompatibleArray() + { + $ossToW3c = [ + WebDriverCapabilityType::PLATFORM => 'platformName', + WebDriverCapabilityType::VERSION => 'browserVersion', + WebDriverCapabilityType::ACCEPT_SSL_CERTS => 'acceptInsecureCerts', + ChromeOptions::CAPABILITY => ChromeOptions::CAPABILITY_W3C, + ]; + + $allowedW3cCapabilities = [ + 'browserName', + 'browserVersion', + 'platformName', + 'acceptInsecureCerts', + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'strictFileInteractability', + 'unhandledPromptBehavior', + ]; + + $ossCapabilities = $this->toArray(); + $w3cCapabilities = []; + + foreach ($ossCapabilities as $capabilityKey => $capabilityValue) { + // Copy already W3C compatible capabilities + if (in_array($capabilityKey, $allowedW3cCapabilities, true)) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + + // Convert capabilitites with changed name + if (array_key_exists($capabilityKey, $ossToW3c)) { + if ($capabilityKey === 'platform') { + $w3cCapabilities[$ossToW3c[$capabilityKey]] = mb_strtolower($capabilityValue); + } else { + $w3cCapabilities[$ossToW3c[$capabilityKey]] = $capabilityValue; + } + } + + // Copy vendor extensions + if (mb_strpos($capabilityKey, ':') !== false) { + $w3cCapabilities[$capabilityKey] = $capabilityValue; + } + } + + // Convert ChromeOptions + if (array_key_exists(ChromeOptions::CAPABILITY, $ossCapabilities)) { + if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $ossCapabilities)) { + $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = array_merge_recursive( + $ossCapabilities[ChromeOptions::CAPABILITY], + $ossCapabilities[ChromeOptions::CAPABILITY_W3C] + ); + } else { + $w3cCapabilities[ChromeOptions::CAPABILITY_W3C] = $ossCapabilities[ChromeOptions::CAPABILITY]; + } + } + + // Convert Firefox profile + if (array_key_exists(FirefoxDriver::PROFILE, $ossCapabilities)) { + // Convert profile only if not already set in moz:firefoxOptions + if (!array_key_exists('moz:firefoxOptions', $ossCapabilities) + || !array_key_exists('profile', $ossCapabilities['moz:firefoxOptions'])) { + $w3cCapabilities['moz:firefoxOptions']['profile'] = $ossCapabilities[FirefoxDriver::PROFILE]; + } + } + + return $w3cCapabilities; + } + /** * @return static */ diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index d2e302f44..1762f44a8 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -15,7 +15,6 @@ namespace Facebook\WebDriver\Remote; -use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Interactions\WebDriverActions; use Facebook\WebDriver\JavaScriptExecutor; use Facebook\WebDriver\WebDriver; @@ -123,12 +122,12 @@ public static function create( // W3C $parameters = [ 'capabilities' => [ - 'firstMatch' => [static::convertCapabilitiesToW3c($desired_capabilities->toArray())], + 'firstMatch' => [$desired_capabilities->toW3cCompatibleArray()], ], ]; - if ($required_capabilities !== null && $required_capabilities_array = $required_capabilities->toArray()) { - $parameters['capabilities']['alwaysMatch'] = static::convertCapabilitiesToW3c($required_capabilities_array); + if ($required_capabilities !== null && !empty($required_capabilities->toArray())) { + $parameters['capabilities']['alwaysMatch'] = $required_capabilities->toW3cCompatibleArray(); } // Legacy protocol @@ -660,28 +659,4 @@ protected static function castToDesiredCapabilitiesObject($desired_capabilities return $desired_capabilities; } - - /** - * Convert keys invalid in W3C capabilities to corresponding ones for W3C. - * - * @param array $capabilitiesArray - * @return array - */ - protected static function convertCapabilitiesToW3c(array $capabilitiesArray) - { - if (array_key_exists(ChromeOptions::CAPABILITY, $capabilitiesArray)) { - if (array_key_exists(ChromeOptions::CAPABILITY_W3C, $capabilitiesArray)) { - $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = array_merge( - $capabilitiesArray[ChromeOptions::CAPABILITY], - $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] - ); - } else { - $capabilitiesArray[ChromeOptions::CAPABILITY_W3C] = $capabilitiesArray[ChromeOptions::CAPABILITY]; - } - - unset($capabilitiesArray[ChromeOptions::CAPABILITY]); - } - - return $capabilitiesArray; - } } diff --git a/tests/unit/Remote/DesiredCapabilitiesTest.php b/tests/unit/Remote/DesiredCapabilitiesTest.php index 6e14ef044..4ef4a055a 100644 --- a/tests/unit/Remote/DesiredCapabilitiesTest.php +++ b/tests/unit/Remote/DesiredCapabilitiesTest.php @@ -15,6 +15,7 @@ namespace Facebook\WebDriver\Remote; +use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Firefox\FirefoxDriver; use Facebook\WebDriver\Firefox\FirefoxPreferences; use Facebook\WebDriver\Firefox\FirefoxProfile; @@ -130,4 +131,140 @@ public function testShouldSetupFirefoxProfileAndDisableReaderViewForFirefoxBrows $this->assertSame('false', $firefoxProfile->getPreference(FirefoxPreferences::READER_PARSE_ON_LOAD_ENABLED)); } + + /** + * @dataProvider provideW3cCapabilities + * @param DesiredCapabilities $inputJsonWireCapabilities + * @param array $expectedW3cCapabilities + */ + public function testShouldConvertCapabilitiesToW3cCompatible( + DesiredCapabilities $inputJsonWireCapabilities, + array $expectedW3cCapabilities + ) { + $this->assertEquals( + $expectedW3cCapabilities, + $inputJsonWireCapabilities->toW3cCompatibleArray() + ); + } + + /** + * @return array[] + */ + public function provideW3cCapabilities() + { + $chromeOptions = new ChromeOptions(); + $chromeOptions->addArguments([ + '--headless', + ]); + + $firefoxProfileEncoded = (new FirefoxProfile())->encode(); + + return [ + 'changed name' => [ + new DesiredCapabilities([ + WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::CHROME, + WebDriverCapabilityType::VERSION => '67.0.1', + WebDriverCapabilityType::PLATFORM => WebDriverPlatform::LINUX, + WebDriverCapabilityType::ACCEPT_SSL_CERTS => true, + ]), + [ + 'browserName' => 'chrome', + 'browserVersion' => '67.0.1', + 'platformName' => 'linux', + 'acceptInsecureCerts' => true, + ], + ], + 'removed capabilitites' => [ + new DesiredCapabilities([ + WebDriverCapabilityType::WEB_STORAGE_ENABLED => true, + WebDriverCapabilityType::TAKES_SCREENSHOT => false, + ]), + [], + ], + 'custom invalid capability should be removed' => [ + new DesiredCapabilities([ + 'customInvalidCapability' => 'shouldBeRemoved', + ]), + [], + ], + 'already W3C capabilitites' => [ + new DesiredCapabilities([ + 'pageLoadStrategy' => 'eager', + 'strictFileInteractability' => false, + ]), + [ + 'pageLoadStrategy' => 'eager', + 'strictFileInteractability' => false, + ], + ], + 'custom vendor extension' => [ + new DesiredCapabilities([ + 'vendor:prefix' => 'vendor extension should be kept', + ]), + [ + 'vendor:prefix' => 'vendor extension should be kept', + ], + ], + 'chromeOptions should be converted' => [ + new DesiredCapabilities([ + ChromeOptions::CAPABILITY => $chromeOptions, + ]), + [ + 'goog:chromeOptions' => [ + 'args' => ['--headless'], + 'binary' => '', + ], + ], + ], + 'chromeOptions should be merged if already defined' => [ + new DesiredCapabilities([ + ChromeOptions::CAPABILITY => $chromeOptions, + ChromeOptions::CAPABILITY_W3C => [ + 'debuggerAddress' => '127.0.0.1:38947', + 'args' => ['window-size=1024,768'], + ], + ]), + [ + 'goog:chromeOptions' => [ + 'args' => ['--headless', 'window-size=1024,768'], + 'binary' => '', + 'debuggerAddress' => '127.0.0.1:38947', + ], + ], + ], + 'firefox_profile should be converted' => [ + new DesiredCapabilities([ + FirefoxDriver::PROFILE => $firefoxProfileEncoded, + ]), + [ + 'moz:firefoxOptions' => [ + 'profile' => $firefoxProfileEncoded, + ], + ], + ], + 'firefox_profile should not be overwritten if already present' => [ + new DesiredCapabilities([ + FirefoxDriver::PROFILE => $firefoxProfileEncoded, + 'moz:firefoxOptions' => ['profile' => 'w3cProfile'], + ]), + [ + 'moz:firefoxOptions' => [ + 'profile' => 'w3cProfile', + ], + ], + ], + 'firefox_profile should be merged with moz:firefoxOptions if they already exists' => [ + new DesiredCapabilities([ + FirefoxDriver::PROFILE => $firefoxProfileEncoded, + 'moz:firefoxOptions' => ['args' => ['-headless']], + ]), + [ + 'moz:firefoxOptions' => [ + 'profile' => $firefoxProfileEncoded, + 'args' => ['-headless'], + ], + ], + ], + ]; + } } From f81659eaa82820b7a55be5ac173f56188af186c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 15 Nov 2019 00:39:53 +0100 Subject: [PATCH 046/373] Run geckodriver tests in headless mode --- tests/functional/WebDriverTestCase.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 8b35a7700..1f125634e 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -65,6 +65,10 @@ protected function setUp() $this->desiredCapabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions); } elseif (getenv('GECKODRIVER') === '1') { $this->serverUrl = 'http://localhost:4444/'; + $this->desiredCapabilities->setCapability( + 'moz:firefoxOptions', + ['args' => ['-headless']] + ); } $this->desiredCapabilities->setBrowserName($browserName); From b4b82e2ccada9a7d251ae4703478e94b5b593370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 15 Nov 2019 00:38:54 +0100 Subject: [PATCH 047/373] Add W3C window size and position commands --- lib/Remote/HttpCommandExecutor.php | 9 ++- tests/functional/WebDriverWindowTest.php | 81 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/functional/WebDriverWindowTest.php diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index b87c94069..d137030a4 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -144,8 +144,9 @@ class HttpCommandExecutor implements WebDriverCommandExecutor 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::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], + DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], 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 => [ @@ -154,11 +155,15 @@ class HttpCommandExecutor implements WebDriverCommandExecutor ], 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::GET_WINDOW_POSITION => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'], DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'], + DriverCommand::MAXIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/maximize'], 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'], + DriverCommand::SET_WINDOW_SIZE => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], + DriverCommand::SET_WINDOW_POSITION => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'], ]; /** * @var string diff --git a/tests/functional/WebDriverWindowTest.php b/tests/functional/WebDriverWindowTest.php new file mode 100644 index 000000000..ccf041b16 --- /dev/null +++ b/tests/functional/WebDriverWindowTest.php @@ -0,0 +1,81 @@ +driver->manage() + ->window() + ->getPosition(); + + $this->assertGreaterThanOrEqual(0, $position->getX()); + $this->assertGreaterThanOrEqual(0, $position->getY()); + } + + public function testShouldGetSize() + { + $size = $this->driver->manage() + ->window() + ->getSize(); + + $this->assertGreaterThan(0, $size->getWidth()); + $this->assertGreaterThan(0, $size->getHeight()); + } + + public function testShouldMaximizeWindow() + { + $sizeBefore = $this->driver->manage() + ->window() + ->getSize(); + + $this->driver->manage() + ->window() + ->maximize(); + + $sizeAfter = $this->driver->manage() + ->window() + ->getSize(); + + $this->assertGreaterThanOrEqual($sizeBefore->getWidth(), $sizeAfter->getWidth()); + $this->assertGreaterThanOrEqual($sizeBefore->getHeight(), $sizeAfter->getHeight()); + } + + public function testShouldSetSize() + { + $sizeBefore = $this->driver->manage() + ->window() + ->getSize(); + $this->assertNotSame(500, $sizeBefore->getWidth()); + $this->assertNotSame(666, $sizeBefore->getHeight()); + + $this->driver->manage() + ->window() + ->setSize(new WebDriverDimension(500, 666)); + + $sizeAfter = $this->driver->manage() + ->window() + ->getSize(); + + $this->assertSame(500, $sizeAfter->getWidth()); + $this->assertSame(666, $sizeAfter->getHeight()); + } + + public function testShouldSetWindowPosition() + { + $this->driver->manage() + ->window() + ->setPosition(new WebDriverPoint(33, 66)); + + $positionAfter = $this->driver->manage() + ->window() + ->getPosition(); + + $this->assertSame(33, $positionAfter->getX()); + $this->assertSame(66, $positionAfter->getY()); + } +} From c7ee5e1852b42d73bc597bb70a1b03344bdcb233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Fri, 15 Nov 2019 12:17:12 +0100 Subject: [PATCH 048/373] Exclude window tests on Saucelabs, because they are not sane --- tests/functional/WebDriverWindowTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/functional/WebDriverWindowTest.php b/tests/functional/WebDriverWindowTest.php index ccf041b16..8de61d3e8 100644 --- a/tests/functional/WebDriverWindowTest.php +++ b/tests/functional/WebDriverWindowTest.php @@ -7,6 +7,9 @@ */ class WebDriverWindowTest extends WebDriverTestCase { + /** + * @group exclude-saucelabs + */ public function testShouldGetPosition() { $position = $this->driver->manage() @@ -45,6 +48,9 @@ public function testShouldMaximizeWindow() $this->assertGreaterThanOrEqual($sizeBefore->getHeight(), $sizeAfter->getHeight()); } + /** + * @group exclude-saucelabs + */ public function testShouldSetSize() { $sizeBefore = $this->driver->manage() From e9f4ec88e7ed10388613aa8b05cf1e3f4d2231d3 Mon Sep 17 00:00:00 2001 From: Lctrs Date: Fri, 22 Nov 2019 00:38:58 +0100 Subject: [PATCH 049/373] Download a compatible version of chromedriver with chrome Following the [algorithm described on the official chromedriver website](https://chromedriver.chromium.org/downloads/version-selection) : > Here are the steps to select the version of ChromeDriver to download: > > - First, find out which version of Chrome you are using. Let's say you have Chrome 72.0.3626.81. > - Take the Chrome version number, remove the last part, and append the result to URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_". For example, with Chrome version 72.0.3626.81, you'd get a URL "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_72.0.3626". > - Use the URL created in the last step to retrieve a small file containing the version of ChromeDriver to use. For example, the above URL will get your a file containing "72.0.3626.69". (The actual number may change in the future, of course.) > - Use the version number retrieved from the previous step to construct the URL to download ChromeDriver. With version 72.0.3626.69, the URL would be "https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/". --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4a8a268a8..0340b80e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -134,7 +134,13 @@ install: - travis_retry composer update --no-interaction $DEPENDENCIES 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 + mkdir chromedriver; + CHROME_VERSION=$(google-chrome --product-version); + CHROME_VERSION=${CHROME_VERSION%.*}; + wget -q -t 3 https://chromedriver.storage.googleapis.com/$(curl -L https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_VERSION})/chromedriver_linux64.zip; + unzip chromedriver_linux64.zip -d chromedriver; + fi - if [ "$BROWSER_NAME" = "chrome" ]; then export CHROMEDRIVER_PATH=$PWD/chromedriver/chromedriver; fi - if [ "$GECKODRIVER" = "1" ]; then mkdir -p 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 # TODO: start only when needed (ie. not in headless mode) From ee14c4392399a9fc7b362f488226141ca67e8348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 19 Nov 2019 13:14:34 +0100 Subject: [PATCH 050/373] Use W3C protocol on some Saucelabs builds --- .travis.yml | 20 +++++++------ tests/functional/WebDriverTestCase.php | 33 +++++++++++++++++----- tests/functional/WebDriverTimeoutsTest.php | 3 ++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0340b80e7..2c80cbb18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -85,8 +85,9 @@ matrix: chrome: stable # Saucelabs builds - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" + - name: 'Sauce Labs, Firefox 47, OSS protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="firefox" VERSION="47.0" PLATFORM="Windows 10" DISABLE_W3C_PROTOCOL="1" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -95,8 +96,9 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" # 74 is the last version which don't use W3C WebDriver by default + - name: 'Sauce Labs, Chrome 74, OSS protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="74.0" PLATFORM="Windows 10" DISABLE_W3C_PROTOCOL="1" # 74 is the last version which don't use W3C WebDriver by default before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -105,8 +107,9 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="75.0" PLATFORM="Windows 10" + - name: 'Sauce Labs, Chrome latest, W3C protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="chrome" VERSION="latest" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" @@ -115,8 +118,9 @@ matrix: jwt: secure: HPq5xFhosa1eSGnaRdJzeyEuaE0mhRlG1gf3G7+dKS0VniF30husSyrxZhbGCCKBGxmIySoAQzd43BCwL69EkUEVKDN87Cpid1Ce9KrSfU3cnN8XIb+4QINyy7x1a47RUAfaaOEx53TrW0ShalvjD+ZwDE8LrgagSox6KQ+nQLE= - - php: '7.3' - env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="16.16299" PLATFORM="Windows 10" + - name: 'Sauce Labs, Edge latest, W3C protocol' + php: '7.3' + env: SAUCELABS=1 BROWSER_NAME="MicrosoftEdge" VERSION="latest" PLATFORM="Windows 10" before_script: - php -S 127.0.0.1:8000 -t tests/functional/web/ &>>./logs/php-server.log & - until $(echo | nc localhost 8000); do sleep 1; echo waiting for PHP server on port 8000...; done; echo "PHP server started" diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index 1f125634e..f012d9068 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -112,9 +112,8 @@ public static function isSauceLabsBuild() public static function isW3cProtocolBuild() { return getenv('GECKODRIVER') === '1' - || (getenv('BROWSER_NAME') === 'chrome' - && getenv('DISABLE_W3C_PROTOCOL') !== '1' - && !self::isSauceLabsBuild()); + || (getenv('BROWSER_NAME') === 'chrome' && getenv('DISABLE_W3C_PROTOCOL') !== '1') + || getenv('BROWSER_NAME') === 'MicrosoftEdge'; } public static function skipForW3cProtocol($message = 'Not supported by W3C specification') @@ -153,12 +152,32 @@ protected function setUpSauceLabs() $this->desiredCapabilities->setBrowserName(getenv('BROWSER_NAME')); $this->desiredCapabilities->setVersion(getenv('VERSION')); $this->desiredCapabilities->setPlatform(getenv('PLATFORM')); - $this->desiredCapabilities->setCapability('name', get_class($this) . '::' . $this->getName()); - $this->desiredCapabilities->setCapability('tags', [get_class($this)]); + $name = get_class($this) . '::' . $this->getName(); + $tags = [get_class($this)]; if (getenv('TRAVIS_JOB_NUMBER')) { - $this->desiredCapabilities->setCapability('tunnel-identifier', getenv('TRAVIS_JOB_NUMBER')); - $this->desiredCapabilities->setCapability('build', getenv('TRAVIS_JOB_NUMBER')); + $tunnelIdentifier = getenv('TRAVIS_JOB_NUMBER'); + $build = getenv('TRAVIS_JOB_NUMBER'); + } + + if (!getenv('DISABLE_W3C_PROTOCOL')) { + $sauceOptions = [ + 'name' => $name, + 'tags' => $tags, + ]; + if (isset($tunnelIdentifier, $build)) { + $sauceOptions['tunnelIdentifier'] = $tunnelIdentifier; + $sauceOptions['build'] = $build; + } + $this->desiredCapabilities->setCapability('sauce:options', (object) $sauceOptions); + } else { + $this->desiredCapabilities->setCapability('name', $name); + $this->desiredCapabilities->setCapability('tags', $tags); + + if (isset($tunnelIdentifier, $build)) { + $this->desiredCapabilities->setCapability('tunnel-identifier', $tunnelIdentifier); + $this->desiredCapabilities->setCapability('build', $build); + } } } } diff --git a/tests/functional/WebDriverTimeoutsTest.php b/tests/functional/WebDriverTimeoutsTest.php index 733be545c..cd3e10d99 100644 --- a/tests/functional/WebDriverTimeoutsTest.php +++ b/tests/functional/WebDriverTimeoutsTest.php @@ -26,6 +26,9 @@ */ class WebDriverTimeoutsTest extends WebDriverTestCase { + /** + * @group exclude-saucelabs + */ public function testShouldFailGettingDelayedElementWithoutWait() { $this->driver->get($this->getTestPageUrl('delayed_element.html')); From a0d6be9a5fb351fe1a554bbd4f4a76f896727414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 19 Nov 2019 13:23:30 +0100 Subject: [PATCH 051/373] Simplify builds --- .travis.yml | 3 --- tests/functional/WebDriverAlertTest.php | 6 ------ tests/functional/WebDriverTestCase.php | 2 +- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c80cbb18..9bb73cc1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,7 +59,6 @@ matrix: php: '7.3' env: - BROWSER_NAME="chrome" - - CHROME_HEADLESS="1" addons: chrome: stable @@ -68,7 +67,6 @@ matrix: php: '7.3' env: - BROWSER_NAME="chrome" - - CHROME_HEADLESS="1" - CHROMEDRIVER="1" addons: chrome: stable @@ -78,7 +76,6 @@ matrix: php: '7.3' env: - BROWSER_NAME="chrome" - - CHROME_HEADLESS="1" - CHROMEDRIVER="1" - DISABLE_W3C_PROTOCOL="1" addons: diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index 163bee157..b99dad1fa 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -25,12 +25,6 @@ class WebDriverAlertTest extends WebDriverTestCase { protected function setUp() { - if (getenv('CHROME_HEADLESS') === '1') { - // Alerts in headless mode should be available in next Chrome version (61), see: - // https://bugs.chromium.org/p/chromium/issues/detail?id=718235 - $this->markTestSkipped('Alerts not yet supported by headless Chrome'); - } - parent::setUp(); $this->driver->get($this->getTestPageUrl('alert.html')); diff --git a/tests/functional/WebDriverTestCase.php b/tests/functional/WebDriverTestCase.php index f012d9068..9b302a1f3 100644 --- a/tests/functional/WebDriverTestCase.php +++ b/tests/functional/WebDriverTestCase.php @@ -113,7 +113,7 @@ public static function isW3cProtocolBuild() { return getenv('GECKODRIVER') === '1' || (getenv('BROWSER_NAME') === 'chrome' && getenv('DISABLE_W3C_PROTOCOL') !== '1') - || getenv('BROWSER_NAME') === 'MicrosoftEdge'; + || (self::isSauceLabsBuild() && getenv('DISABLE_W3C_PROTOCOL') !== '1'); } public static function skipForW3cProtocol($message = 'Not supported by W3C specification') From b54d50371b68926a60d2832db2ed94c1d3e95c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 23 Nov 2019 14:11:55 +0100 Subject: [PATCH 052/373] Skip tests utilizing xPath selectors, which are not properly supported in Edge --- tests/functional/WebDriverCheckboxesTest.php | 1 + tests/functional/WebDriverRadiosTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index 48b72a53c..91513bd94 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -20,6 +20,7 @@ /** * @covers \Facebook\WebDriver\WebDriverCheckboxes * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @group exclude-edge */ class WebDriverCheckboxesTest extends WebDriverTestCase { diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index 4d659772b..cf7b8378c 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -21,6 +21,7 @@ /** * @covers \Facebook\WebDriver\WebDriverRadios * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @group exclude-edge */ class WebDriverRadiosTest extends WebDriverTestCase { From a419816547eed70cfcf54fd0763df33c94adcce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sat, 23 Nov 2019 17:56:26 +0100 Subject: [PATCH 053/373] Skip file upload tests on Saucelabs, because W3C protocol does not support remote file upload See https://github.com/w3c/webdriver/issues/1355 --- lib/Remote/RemoteWebElement.php | 27 ++++++++++++--------------- tests/functional/FileUploadTest.php | 2 ++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/Remote/RemoteWebElement.php b/lib/Remote/RemoteWebElement.php index 75dfa3ba2..33286504b 100644 --- a/lib/Remote/RemoteWebElement.php +++ b/lib/Remote/RemoteWebElement.php @@ -358,6 +358,7 @@ public function isSelected() public function sendKeys($value) { $local_file = $this->fileDetector->getLocalFile($value); + if ($local_file === null) { if ($this->isW3cCompliant) { $params = [ @@ -370,22 +371,18 @@ public function sendKeys($value) ':id' => $this->id, ]; } - - $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); - - return $this; - } - - if ($this->isW3cCompliant) { - $params = [ - 'text' => $local_file, - ':id' => $this->id, - ]; } else { - $params = [ - 'value' => WebDriverKeys::encode($this->upload($local_file)), - ':id' => $this->id, - ]; + if ($this->isW3cCompliant) { + $params = [ + 'text' => $local_file, + ':id' => $this->id, + ]; + } else { + $params = [ + 'value' => WebDriverKeys::encode($this->upload($local_file)), + ':id' => $this->id, + ]; + } } $this->executor->execute(DriverCommand::SEND_KEYS_TO_ELEMENT, $params); diff --git a/tests/functional/FileUploadTest.php b/tests/functional/FileUploadTest.php index 5b2f6d2d3..1029a2363 100644 --- a/tests/functional/FileUploadTest.php +++ b/tests/functional/FileUploadTest.php @@ -26,6 +26,8 @@ class FileUploadTest extends WebDriverTestCase /** * @group exclude-edge * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6052385/ + * @group exclude-saucelabs + * W3C protocol does not support remote file upload: https://github.com/w3c/webdriver/issues/1355 */ public function testShouldUploadAFile() { From 9d5623f40ab320601f8b2cee2bb463d722bb4393 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 25 Nov 2019 15:10:50 +0800 Subject: [PATCH 054/373] Fix mouseUp W3C action --- lib/Remote/RemoteMouse.php | 2 +- tests/functional/WebDriverActionsTest.php | 24 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Remote/RemoteMouse.php b/lib/Remote/RemoteMouse.php index 4bd4a849b..6b7375834 100644 --- a/lib/Remote/RemoteMouse.php +++ b/lib/Remote/RemoteMouse.php @@ -241,7 +241,7 @@ public function mouseUp(WebDriverCoordinates $where = null) 'parameters' => ['pointerType' => 'mouse'], 'actions' => array_merge($moveAction, [ [ - 'type' => 'pointerDown', + 'type' => 'pointerUp', 'duration' => 0, 'button' => 0, ], diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 9798f6e5a..6799ef1d3 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -136,6 +136,30 @@ public function testShouldDoubleClickOnElement() $this->assertContains('dblclick item-3', $this->retrieveLoggedEvents()); } + /** + * @covers ::__construct + * @covers ::dragAndDrop + * @covers ::perform + */ + public function testShouldDragAndDrop() + { + if ($this->desiredCapabilities->getBrowserName() === WebDriverBrowserType::HTMLUNIT) { + $this->markTestSkipped('Not supported by HtmlUnit browser'); + } + + $element = $this->driver->findElement(WebDriverBy::id('item-3')); + $target = $this->driver->findElement(WebDriverBy::id('item-1')); + + $this->driver->action() + ->dragAndDrop($element, $target) + ->perform(); + + $this->assertContains('mouseover item-3', $this->retrieveLoggedEvents()); + $this->assertContains('mousedown item-3', $this->retrieveLoggedEvents()); + $this->assertContains('mouseover item-1', $this->retrieveLoggedEvents()); + $this->assertContains('mouseup item-1', $this->retrieveLoggedEvents()); + } + /** * @return array */ From a94e33b99a8330c90c56128e430ffc55c507d304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 25 Nov 2019 17:13:57 +0100 Subject: [PATCH 055/373] Exclude unstable drag and drop test on SauceLabs --- tests/functional/WebDriverActionsTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index 6799ef1d3..cb4359183 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -140,6 +140,7 @@ public function testShouldDoubleClickOnElement() * @covers ::__construct * @covers ::dragAndDrop * @covers ::perform + * @group exclude-saucelabs */ public function testShouldDragAndDrop() { From 574684f29b8d513ae213c4567f2f71da92734db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Sun, 24 Nov 2019 10:10:35 +0100 Subject: [PATCH 056/373] Add all W3C WebDriver exceptions --- .../ElementClickInterceptedException.php | 24 +++++ .../ElementNotInteractableException.php | 23 +++++ .../ElementNotSelectableException.php | 5 +- lib/Exception/ElementNotVisibleException.php | 3 + lib/Exception/ExpectedException.php | 3 + .../IMEEngineActivationFailedException.php | 3 + lib/Exception/IMENotAvailableException.php | 3 + lib/Exception/IndexOutOfBoundsException.php | 3 + .../InsecureCertificateException.php | 24 +++++ lib/Exception/InvalidArgumentException.php | 23 +++++ .../InvalidCookieDomainException.php | 3 + lib/Exception/InvalidCoordinatesException.php | 3 + .../InvalidElementStateException.php | 4 + lib/Exception/InvalidSelectorException.php | 3 + lib/Exception/InvalidSessionIdException.php | 24 +++++ lib/Exception/JavascriptErrorException.php | 23 +++++ .../MoveTargetOutOfBoundsException.php | 3 + lib/Exception/NoAlertOpenException.php | 5 +- lib/Exception/NoCollectionException.php | 3 + lib/Exception/NoScriptResultException.php | 3 + lib/Exception/NoStringException.php | 3 + lib/Exception/NoStringLengthException.php | 3 + lib/Exception/NoStringWrapperException.php | 3 + lib/Exception/NoSuchAlertException.php | 23 +++++ lib/Exception/NoSuchCollectionException.php | 3 + lib/Exception/NoSuchCookieException.php | 24 +++++ lib/Exception/NoSuchDocumentException.php | 5 +- lib/Exception/NoSuchDriverException.php | 3 + lib/Exception/NoSuchElementException.php | 3 + lib/Exception/NoSuchFrameException.php | 3 + lib/Exception/NoSuchWindowException.php | 3 + lib/Exception/NullPointerException.php | 3 + lib/Exception/ScriptTimeoutException.php | 3 + lib/Exception/SessionNotCreatedException.php | 3 + .../StaleElementReferenceException.php | 3 + lib/Exception/TimeoutException.php | 23 +++++ ...php => UnableToCaptureScreenException.php} | 5 +- lib/Exception/UnableToSetCookieException.php | 3 + .../UnexpectedAlertOpenException.php | 3 + .../UnexpectedJavascriptException.php | 5 +- lib/Exception/UnknownCommandException.php | 3 + lib/Exception/UnknownErrorException.php | 23 +++++ lib/Exception/UnknownMethodException.php | 23 +++++ lib/Exception/UnknownServerException.php | 5 +- .../UnsupportedOperationException.php | 3 + lib/Exception/WebDriverException.php | 92 ++++++++++++------- lib/Exception/XPathLookupException.php | 3 + lib/Net/URLChecker.php | 6 +- lib/WebDriverWait.php | 6 +- tests/functional/WebDriverAlertTest.php | 8 +- tests/functional/WebDriverTimeoutsTest.php | 6 +- .../unit/Exception/WebDriverExceptionTest.php | 49 ++++++++-- 52 files changed, 487 insertions(+), 55 deletions(-) create mode 100644 lib/Exception/ElementClickInterceptedException.php create mode 100644 lib/Exception/ElementNotInteractableException.php create mode 100644 lib/Exception/InsecureCertificateException.php create mode 100644 lib/Exception/InvalidArgumentException.php create mode 100644 lib/Exception/InvalidSessionIdException.php create mode 100644 lib/Exception/JavascriptErrorException.php create mode 100644 lib/Exception/NoSuchAlertException.php create mode 100644 lib/Exception/NoSuchCookieException.php create mode 100644 lib/Exception/TimeoutException.php rename lib/Exception/{TimeOutException.php => UnableToCaptureScreenException.php} (85%) create mode 100644 lib/Exception/UnknownErrorException.php create mode 100644 lib/Exception/UnknownMethodException.php diff --git a/lib/Exception/ElementClickInterceptedException.php b/lib/Exception/ElementClickInterceptedException.php new file mode 100644 index 000000000..e4b3a529e --- /dev/null +++ b/lib/Exception/ElementClickInterceptedException.php @@ -0,0 +1,24 @@ +driver->switchTo()->alert()->accept(); - $this->expectException(NoAlertOpenException::class); + if (self::isW3cProtocolBuild()) { + $this->expectException(NoSuchAlertException::class); + } else { + $this->expectException(NoAlertOpenException::class); + } + $this->driver->switchTo()->alert()->accept(); } diff --git a/tests/functional/WebDriverTimeoutsTest.php b/tests/functional/WebDriverTimeoutsTest.php index cd3e10d99..ca85075b8 100644 --- a/tests/functional/WebDriverTimeoutsTest.php +++ b/tests/functional/WebDriverTimeoutsTest.php @@ -17,7 +17,7 @@ use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Exception\ScriptTimeoutException; -use Facebook\WebDriver\Exception\TimeOutException; +use Facebook\WebDriver\Exception\TimeoutException; use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\Remote\WebDriverBrowserType; @@ -65,8 +65,8 @@ public function testShouldFailIfPageIsLoadingLongerThanPageLoadTimeout() try { $this->driver->get($this->getTestPageUrl('slow_loading.html')); - $this->fail('ScriptTimeoutException or TimeOutException exception should be thrown'); - } catch (TimeOutException $e) { // thrown by Selenium 3.0.0+ + $this->fail('ScriptTimeoutException or TimeoutException exception should be thrown'); + } catch (TimeoutException $e) { // thrown by Selenium 3.0.0+ } catch (ScriptTimeoutException $e) { // thrown by Selenium 2 } } diff --git a/tests/unit/Exception/WebDriverExceptionTest.php b/tests/unit/Exception/WebDriverExceptionTest.php index f9aadd207..be3ab964c 100644 --- a/tests/unit/Exception/WebDriverExceptionTest.php +++ b/tests/unit/Exception/WebDriverExceptionTest.php @@ -29,14 +29,15 @@ public function testShouldStoreResultsOnInstantiation() } /** - * @dataProvider statusCodeProvider - * @param int $statusCode + * @dataProvider jsonWireStatusCodeProvider + * @dataProvider w3CWebDriverErrorCodeProvider + * @param int $errorCode * @param string $expectedExceptionType */ - public function testShouldThrowProperExceptionBasedOnSeleniumStatusCode($statusCode, $expectedExceptionType) + public function testShouldThrowProperExceptionBasedOnWebDriverErrorCode($errorCode, $expectedExceptionType) { try { - WebDriverException::throwException($statusCode, 'exception message', ['results']); + WebDriverException::throwException($errorCode, 'exception message', ['results']); } catch (WebDriverException $e) { $this->assertInstanceOf($expectedExceptionType, $e); @@ -48,7 +49,7 @@ public function testShouldThrowProperExceptionBasedOnSeleniumStatusCode($statusC /** * @return array[] */ - public function statusCodeProvider() + public function jsonWireStatusCodeProvider() { return [ [1337, UnrecognizedExceptionException::class], @@ -72,7 +73,7 @@ public function statusCodeProvider() [18, NoScriptResultException::class], [19, XPathLookupException::class], [20, NoSuchCollectionException::class], - [21, TimeOutException::class], + [21, TimeoutException::class], [22, NullPointerException::class], [23, NoSuchWindowException::class], [24, InvalidCookieDomainException::class], @@ -88,4 +89,40 @@ public function statusCodeProvider() [34, MoveTargetOutOfBoundsException::class], ]; } + + /** + * @return array[] + */ + public function w3CWebDriverErrorCodeProvider() + { + return [ + ['element click intercepted', ElementClickInterceptedException::class], + ['element not interactable', ElementNotInteractableException::class], + ['element not interactable', ElementNotInteractableException::class], + ['insecure certificate', InsecureCertificateException::class], + ['invalid argument', InvalidArgumentException::class], + ['invalid cookie domain', InvalidCookieDomainException::class], + ['invalid element state', InvalidElementStateException::class], + ['invalid selector', InvalidSelectorException::class], + ['invalid session id', InvalidSessionIdException::class], + ['javascript error', JavascriptErrorException::class], + ['move target out of bounds', MoveTargetOutOfBoundsException::class], + ['no such alert', NoSuchAlertException::class], + ['no such cookie', NoSuchCookieException::class], + ['no such element', NoSuchElementException::class], + ['no such frame', NoSuchFrameException::class], + ['no such window', NoSuchWindowException::class], + ['script timeout', ScriptTimeoutException::class], + ['session not created', SessionNotCreatedException::class], + ['stale element reference', StaleElementReferenceException::class], + ['timeout', TimeoutException::class], + ['unable to set cookie', UnableToSetCookieException::class], + ['unable to capture screen', UnableToCaptureScreenException::class], + ['unexpected alert open', UnexpectedAlertOpenException::class], + ['unknown command', UnknownCommandException::class], + ['unknown error', UnknownErrorException::class], + ['unknown method', UnknownMethodException::class], + ['unsupported operation', UnsupportedOperationException::class], + ]; + } } From 88fff59dac637265906d100fb91142d3f5475aca Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 21 Nov 2019 09:32:34 +0800 Subject: [PATCH 057/373] Pass W3C Compliance to RemoteTargetLocator --- lib/Remote/RemoteTargetLocator.php | 11 +++++++---- lib/Remote/RemoteWebDriver.php | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 8bd89ef44..1f8cb9270 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -33,11 +33,16 @@ class RemoteTargetLocator implements WebDriverTargetLocator * @var WebDriver */ protected $driver; + /** + * @var bool + */ + protected $isW3cCompliant; - public function __construct($executor, $driver) + public function __construct($executor, $driver, $isW3cCompliant = false) { $this->executor = $executor; $this->driver = $driver; + $this->isW3cCompliant = $isW3cCompliant; } /** @@ -112,8 +117,6 @@ public function activeElement() $response = $this->driver->execute(DriverCommand::GET_ACTIVE_ELEMENT, []); $method = new RemoteExecuteMethod($this->driver); - $isW3cCompliant = ($this->driver instanceof RemoteWebDriver) ? $this->driver->isW3cCompliant() : false; - - return new RemoteWebElement($method, JsonWireCompat::getElement($response), $isW3cCompliant); + return new RemoteWebElement($method, JsonWireCompat::getElement($response), $this->isW3cCompliant); } } diff --git a/lib/Remote/RemoteWebDriver.php b/lib/Remote/RemoteWebDriver.php index 1762f44a8..3164bad5e 100644 --- a/lib/Remote/RemoteWebDriver.php +++ b/lib/Remote/RemoteWebDriver.php @@ -432,7 +432,7 @@ public function navigate() */ public function switchTo() { - return new RemoteTargetLocator($this->getExecuteMethod(), $this); + return new RemoteTargetLocator($this->getExecuteMethod(), $this, $this->isW3cCompliant); } /** From 57f871eaa558b1108ece0fc4a713dd57796e4d50 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 20 Nov 2019 20:54:38 +0800 Subject: [PATCH 058/373] W3C switchToWindow takes a handle The W3C specification dictates that the parameter used to select the window should be named `handle`. Source: https://www.w3.org/TR/webdriver/#switch-to-window --- lib/Remote/RemoteTargetLocator.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Remote/RemoteTargetLocator.php b/lib/Remote/RemoteTargetLocator.php index 1f8cb9270..7e8dbcd1f 100644 --- a/lib/Remote/RemoteTargetLocator.php +++ b/lib/Remote/RemoteTargetLocator.php @@ -89,7 +89,12 @@ public function frame($frame) */ public function window($handle) { - $params = ['name' => (string) $handle]; + if ($this->isW3cCompliant) { + $params = ['handle' => (string) $handle]; + } else { + $params = ['name' => (string) $handle]; + } + $this->executor->execute(DriverCommand::SWITCH_TO_WINDOW, $params); return $this->driver; From 98686d91d96db975e1e22f6a8bdc5ccda2b54319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Mon, 25 Nov 2019 18:54:33 +0100 Subject: [PATCH 059/373] Add tests for switchTo()->window() of RemoteTargetLocator --- tests/functional/RemoteTargetLocatorTest.php | 55 ++++++++++++++++++++ tests/functional/WebDriverAlertTest.php | 1 + tests/functional/web/index.html | 2 + tests/functional/web/open_new_window.html | 2 +- 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/functional/RemoteTargetLocatorTest.php diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php new file mode 100644 index 000000000..210e90b18 --- /dev/null +++ b/tests/functional/RemoteTargetLocatorTest.php @@ -0,0 +1,55 @@ +driver->get($this->getTestPageUrl('open_new_window.html')); + $originalWindowHandle = $this->driver->getWindowHandle(); + $windowHandlesBefore = $this->driver->getWindowHandles(); + + $this->driver->findElement(WebDriverBy::cssSelector('a#open-new-window')) + ->click(); + + $this->driver->wait()->until( + WebDriverExpectedCondition::numberOfWindowsToBe(2) + ); + + // At first the window should not be switched + $this->assertContains('open_new_window.html', $this->driver->getCurrentURL()); + $this->assertSame($originalWindowHandle, $this->driver->getWindowHandle()); + + /** + * @see https://w3c.github.io/webdriver/#get-window-handles + * > "The order in which the window handles are returned is arbitrary." + * Thus we must first find out which window handle is the new one + */ + $windowHandlesAfter = $this->driver->getWindowHandles(); + $newWindowHandle = array_diff($windowHandlesAfter, $windowHandlesBefore); + $newWindowHandle = reset($newWindowHandle); + + $this->driver->switchTo()->window($newWindowHandle); + + // After switchTo() is called, the active window should be changed + $this->assertContains('index.html', $this->driver->getCurrentURL()); + $this->assertNotSame($originalWindowHandle, $this->driver->getWindowHandle()); + } +} diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index b5fd3dec5..72d38d82a 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -21,6 +21,7 @@ /** * @covers \Facebook\WebDriver\WebDriverAlert + * @covers \Facebook\WebDriver\Remote\RemoteTargetLocator */ class WebDriverAlertTest extends WebDriverTestCase { diff --git a/tests/functional/web/index.html b/tests/functional/web/index.html index cf5b9c6c1..ff80fe025 100644 --- a/tests/functional/web/index.html +++ b/tests/functional/web/index.html @@ -11,6 +11,8 @@

Welcome to the facebook/php-webdriver testing page.

| Form with file upload | + Form with checkboxes and radios + | New window opener test page | Delayed render diff --git a/tests/functional/web/open_new_window.html b/tests/functional/web/open_new_window.html index b56e93d69..3c790226a 100644 --- a/tests/functional/web/open_new_window.html +++ b/tests/functional/web/open_new_window.html @@ -5,6 +5,6 @@ php-webdriver test page - open new window + open new window From 51353795e94ed34d23f2657bb690827a072ad14a Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 21 Nov 2019 08:32:22 +0800 Subject: [PATCH 060/373] Make GET_ACTIVE_ELEMENT a GET for W3C As defined in the specification 12.2.6: HTTP Method URI Template GET /session/{session id}/element/active Source: https://www.w3.org/TR/webdriver/#get-active-element --- lib/Remote/HttpCommandExecutor.php | 1 + tests/functional/RemoteTargetLocatorTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/Remote/HttpCommandExecutor.php b/lib/Remote/HttpCommandExecutor.php index d137030a4..a1b05c5ff 100644 --- a/lib/Remote/HttpCommandExecutor.php +++ b/lib/Remote/HttpCommandExecutor.php @@ -146,6 +146,7 @@ class HttpCommandExecutor implements WebDriverCommandExecutor DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'], DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'], DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'], + DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'], DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'], DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'], DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'], diff --git a/tests/functional/RemoteTargetLocatorTest.php b/tests/functional/RemoteTargetLocatorTest.php index 210e90b18..becc194c8 100644 --- a/tests/functional/RemoteTargetLocatorTest.php +++ b/tests/functional/RemoteTargetLocatorTest.php @@ -15,6 +15,8 @@ namespace Facebook\WebDriver; +use Facebook\WebDriver\Remote\RemoteWebElement; + /** * @covers \Facebook\WebDriver\Remote\RemoteTargetLocator */ @@ -52,4 +54,21 @@ public function testShouldSwitchToWindow() $this->assertContains('index.html', $this->driver->getCurrentURL()); $this->assertNotSame($originalWindowHandle, $this->driver->getWindowHandle()); } + + /** + * @cover ::activeElement + */ + public function testActiveElement() + { + $this->driver->get($this->getTestPageUrl('index.html')); + + $activeElement = $this->driver->switchTo()->activeElement(); + $this->assertInstanceOf(RemoteWebElement::class, $activeElement); + $this->assertSame('body', $activeElement->getTagName()); + + $this->driver->findElement(WebDriverBy::name('test_name'))->click(); + $activeElement = $this->driver->switchTo()->activeElement(); + $this->assertSame('input', $activeElement->getTagName()); + $this->assertSame('test_name', $activeElement->getAttribute('name')); + } } From 1dcec4bcda282ae4ea6a4e1314a1cd3c953974d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Machulda?= Date: Tue, 26 Nov 2019 17:12:40 +0100 Subject: [PATCH 061/373] Minor codestyle improvements and unifications --- .php_cs.dist | 10 +++++++++- phpstan.neon | 16 ++++++++-------- tests/functional/RemoteWebDriverCreateTest.php | 2 +- tests/functional/RemoteWebDriverTest.php | 2 +- tests/functional/WebDriverActionsTest.php | 2 +- tests/functional/WebDriverAlertTest.php | 2 +- tests/functional/WebDriverByTest.php | 7 +++++-- tests/functional/WebDriverCheckboxesTest.php | 14 +++++++------- tests/functional/WebDriverNavigationTest.php | 2 +- tests/functional/WebDriverRadiosTest.php | 14 +++++++------- tests/functional/WebDriverSelectTest.php | 9 ++++++--- tests/functional/WebDriverTimeoutsTest.php | 4 ++-- tests/unit/CookieTest.php | 4 ++-- tests/unit/Exception/WebDriverExceptionTest.php | 8 ++++---- .../WebDriverButtonReleaseActionTest.php | 4 ++-- .../Internal/WebDriverClickActionTest.php | 4 ++-- .../Internal/WebDriverClickAndHoldActionTest.php | 4 ++-- .../Internal/WebDriverContextClickActionTest.php | 4 ++-- .../Internal/WebDriverCoordinatesTest.php | 10 +++++----- .../Internal/WebDriverDoubleClickActionTest.php | 4 ++-- .../Internal/WebDriverKeyDownActionTest.php | 4 ++-- .../Internal/WebDriverKeyUpActionTest.php | 4 ++-- .../Internal/WebDriverMouseMoveActionTest.php | 4 ++-- .../WebDriverMouseToOffsetActionTest.php | 4 ++-- .../Internal/WebDriverSendKeysActionTest.php | 4 ++-- tests/unit/Remote/DesiredCapabilitiesTest.php | 6 +++--- tests/unit/Remote/HttpCommandExecutorTest.php | 6 +++--- tests/unit/Remote/RemoteWebDriverTest.php | 2 +- tests/unit/Support/XPathEscaperTest.php | 4 ++-- tests/unit/WebDriverOptionsTest.php | 2 +- 30 files changed, 90 insertions(+), 76 deletions(-) diff --git a/.php_cs.dist b/.php_cs.dist index 48aa29de7..b771f8b04 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -13,6 +13,8 @@ return PhpCsFixer\Config::create() 'concat_space' => ['spacing' => 'one'], 'function_typehint_space' => true, 'general_phpdoc_annotation_remove' => ['author'], + 'implode_call' => true, + 'is_null' => true, 'linebreak_after_opening_tag' => true, 'lowercase_cast' => true, 'mb_str_functions' => true, @@ -51,9 +53,15 @@ return PhpCsFixer\Config::create() 'ordered_imports' => true, 'php_unit_construct' => true, 'php_unit_dedicate_assert' => true, - 'php_unit_expectation' => true, + 'php_unit_expectation' => ['target' => '5.6'], + 'php_unit_method_casing' => ['case' => 'camel_case'], 'php_unit_mock' => true, + 'php_unit_mock_short_will_return' => true, + 'php_unit_namespaced' => ['target' => '5.7'], 'php_unit_no_expectation_annotation' => true, + 'php_unit_ordered_covers' => true, + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], 'phpdoc_add_missing_param_annotation' => true, 'phpdoc_indent' => true, 'phpdoc_no_access' => true, diff --git a/phpstan.neon b/phpstan.neon index 7023c466e..3ea02aee5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,10 @@ parameters: - ignoreErrors: - - '#Class Symfony\\Component\\Process\\ProcessBuilder not found.#' - - '#Instantiated class Symfony\\Component\\Process\\ProcessBuilder not found.#' - - '#Call to method setPrefix\(\) on an unknown class Symfony\\Component\\Process\\ProcessBuilder#' + ignoreErrors: + - '#Class Symfony\\Component\\Process\\ProcessBuilder not found.#' + - '#Instantiated class Symfony\\Component\\Process\\ProcessBuilder not found.#' + - '#Call to method setPrefix\(\) on an unknown class Symfony\\Component\\Process\\ProcessBuilder#' # To be fixed: - - '#Call to an undefined method RecursiveIteratorIterator::getSubPathName\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#' - - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#' + - '#Call to an undefined method RecursiveIteratorIterator::getSubPathName\(\)#' + - '#Call to an undefined method Facebook\\WebDriver\\WebDriver::getTouch\(\)#' + - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::getCoordinates\(\)#' + - '#Call to an undefined method Facebook\\WebDriver\\WebDriverElement::equals\(\)#' diff --git a/tests/functional/RemoteWebDriverCreateTest.php b/tests/functional/RemoteWebDriverCreateTest.php index a2f6e9d0f..5abec763b 100644 --- a/tests/functional/RemoteWebDriverCreateTest.php +++ b/tests/functional/RemoteWebDriverCreateTest.php @@ -20,8 +20,8 @@ use Facebook\WebDriver\Remote\RemoteWebDriver; /** - * @covers \Facebook\WebDriver\Remote\RemoteWebDriver * @covers \Facebook\WebDriver\Remote\HttpCommandExecutor + * @covers \Facebook\WebDriver\Remote\RemoteWebDriver */ class RemoteWebDriverCreateTest extends WebDriverTestCase { diff --git a/tests/functional/RemoteWebDriverTest.php b/tests/functional/RemoteWebDriverTest.php index 72fc30505..6f3becb0b 100644 --- a/tests/functional/RemoteWebDriverTest.php +++ b/tests/functional/RemoteWebDriverTest.php @@ -38,8 +38,8 @@ public function testShouldGetPageTitle() } /** - * @covers ::getCurrentURL * @covers ::get + * @covers ::getCurrentURL */ public function testShouldGetCurrentUrl() { diff --git a/tests/functional/WebDriverActionsTest.php b/tests/functional/WebDriverActionsTest.php index cb4359183..deb700575 100644 --- a/tests/functional/WebDriverActionsTest.php +++ b/tests/functional/WebDriverActionsTest.php @@ -61,8 +61,8 @@ public function testShouldClickOnElement() /** * @covers ::__construct * @covers ::clickAndHold - * @covers ::release * @covers ::perform + * @covers ::release */ public function testShouldClickAndHoldOnElementAndRelease() { diff --git a/tests/functional/WebDriverAlertTest.php b/tests/functional/WebDriverAlertTest.php index 72d38d82a..d6f5c91a2 100644 --- a/tests/functional/WebDriverAlertTest.php +++ b/tests/functional/WebDriverAlertTest.php @@ -20,8 +20,8 @@ use Facebook\WebDriver\Remote\WebDriverBrowserType; /** - * @covers \Facebook\WebDriver\WebDriverAlert * @covers \Facebook\WebDriver\Remote\RemoteTargetLocator + * @covers \Facebook\WebDriver\WebDriverAlert */ class WebDriverAlertTest extends WebDriverTestCase { diff --git a/tests/functional/WebDriverByTest.php b/tests/functional/WebDriverByTest.php index 028db7920..4ff114751 100644 --- a/tests/functional/WebDriverByTest.php +++ b/tests/functional/WebDriverByTest.php @@ -24,7 +24,7 @@ class WebDriverByTest extends WebDriverTestCase { /** - * @dataProvider textElementsProvider + * @dataProvider provideTextElements * @param string $webDriverByLocatorMethod * @param string $webDriverByLocatorValue * @param string $expectedText @@ -52,7 +52,10 @@ public function testShouldFindTextElementByLocator( } } - public function textElementsProvider() + /** + * @return array[] + */ + public function provideTextElements() { return [ 'id' => ['id', 'id_test', 'Test by ID'], diff --git a/tests/functional/WebDriverCheckboxesTest.php b/tests/functional/WebDriverCheckboxesTest.php index 91513bd94..0ddcccd0d 100644 --- a/tests/functional/WebDriverCheckboxesTest.php +++ b/tests/functional/WebDriverCheckboxesTest.php @@ -18,8 +18,8 @@ use Facebook\WebDriver\Exception\NoSuchElementException; /** - * @covers \Facebook\WebDriver\WebDriverCheckboxes * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @covers \Facebook\WebDriver\WebDriverCheckboxes * @group exclude-edge */ class WebDriverCheckboxesTest extends WebDriverTestCase @@ -137,7 +137,7 @@ public function testSelectByIndexInvalid() } /** - * @dataProvider selectByVisibleTextDataProvider + * @dataProvider provideSelectByVisibleTextData * * @param string $text * @param string $value @@ -154,9 +154,9 @@ public function testSelectByVisibleText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisibleTextDataProvider() + public function provideSelectByVisibleTextData() { return [ ['J 2 B', 'j2b'], @@ -165,7 +165,7 @@ public function selectByVisibleTextDataProvider() } /** - * @dataProvider selectByVisiblePartialTextDataProvider + * @dataProvider provideSelectByVisiblePartialTextData * * @param string $text * @param string $value @@ -182,9 +182,9 @@ public function testSelectByVisiblePartialText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisiblePartialTextDataProvider() + public function provideSelectByVisiblePartialTextData() { return [ ['2 B', 'j2b'], diff --git a/tests/functional/WebDriverNavigationTest.php b/tests/functional/WebDriverNavigationTest.php index d3aaebcb6..c2286f3b2 100644 --- a/tests/functional/WebDriverNavigationTest.php +++ b/tests/functional/WebDriverNavigationTest.php @@ -21,8 +21,8 @@ class WebDriverNavigationTest extends WebDriverTestCase { /** - * @covers ::to * @covers ::__construct + * @covers ::to */ public function testShouldNavigateToUrl() { diff --git a/tests/functional/WebDriverRadiosTest.php b/tests/functional/WebDriverRadiosTest.php index cf7b8378c..bb0eaa41e 100644 --- a/tests/functional/WebDriverRadiosTest.php +++ b/tests/functional/WebDriverRadiosTest.php @@ -19,8 +19,8 @@ use Facebook\WebDriver\Exception\UnsupportedOperationException; /** - * @covers \Facebook\WebDriver\WebDriverRadios * @covers \Facebook\WebDriver\AbstractWebDriverCheckboxOrRadio + * @covers \Facebook\WebDriver\WebDriverRadios * @group exclude-edge */ class WebDriverRadiosTest extends WebDriverTestCase @@ -115,7 +115,7 @@ public function testSelectByIndexInvalid() } /** - * @dataProvider selectByVisibleTextDataProvider + * @dataProvider provideSelectByVisibleTextData * * @param string $text * @param string $value @@ -128,9 +128,9 @@ public function testSelectByVisibleText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisibleTextDataProvider() + public function provideSelectByVisibleTextData() { return [ ['J 3 B', 'j3b'], @@ -139,7 +139,7 @@ public function selectByVisibleTextDataProvider() } /** - * @dataProvider selectByVisiblePartialTextDataProvider + * @dataProvider provideSelectByVisiblePartialTextData * * @param string $text * @param string $value @@ -152,9 +152,9 @@ public function testSelectByVisiblePartialText($text, $value) } /** - * @return array + * @return array[] */ - public function selectByVisiblePartialTextDataProvider() + public function provideSelectByVisiblePartialTextData() { return [ ['3 B', 'j3b'], diff --git a/tests/functional/WebDriverSelectTest.php b/tests/functional/WebDriverSelectTest.php index b25dd03af..8d96a1944 100644 --- a/tests/functional/WebDriverSelectTest.php +++ b/tests/functional/WebDriverSelectTest.php @@ -21,8 +21,8 @@ /** * @group exclude-saucelabs - * @covers \Facebook\WebDriver\WebDriverSelect * @covers \Facebook\WebDriver\Exception\UnexpectedTagNameException + * @covers \Facebook\WebDriver\WebDriverSelect */ class WebDriverSelectTest extends WebDriverTestCase { @@ -58,7 +58,7 @@ public function testShouldThrowExceptionWhenNotInstantiatedOnSelectElement() } /** - * @dataProvider selectSelectorProvider + * @dataProvider provideSelectSelector * @param string $selector */ public function testShouldGetOptionsOfSelect($selector) @@ -72,7 +72,10 @@ public function testShouldGetOptionsOfSelect($selector) $this->assertCount(5, $options); } - public function selectSelectorProvider() + /** + * @return array[] + */ + public function provideSelectSelector() { return [ 'simple + +
+