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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ indent_size = 2

[*.php]
ij_php_align_multiline_parameters = false

[*.neon]
indent_style = tab
37 changes: 27 additions & 10 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
parameters:
level: 8
paths:
- src
- tests
checkMissingIterableValueType: false
treatPhpDocTypesAsCertain: false

includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon

parameters:
level: 9
paths:
- src
- tests
ignoreErrors:
-
# See: https://github.com/php-webdriver/php-webdriver/pull/1120
message: '#^Parameter \#1 \$seconds of method Facebook\\WebDriver\\WebDriverTimeouts\:\:implicitlyWait\(\) expects int, float\|int given\.$#'
identifier: argument.type
count: 1
path: src/WebdriverClassicDriver.php
-
# See: https://github.com/php-webdriver/php-webdriver/pull/1120
message: '#^Parameter \#1 \$seconds of method Facebook\\WebDriver\\WebDriverTimeouts\:\:pageLoadTimeout\(\) expects int, float\|int given\.$#'
identifier: argument.type
count: 1
path: src/WebdriverClassicDriver.php
-
# See: https://github.com/php-webdriver/php-webdriver/pull/1120
message: '#^Parameter \#1 \$seconds of method Facebook\\WebDriver\\WebDriverTimeouts\:\:setScriptTimeout\(\) expects int, float\|int given\.$#'
identifier: argument.type
count: 1
path: src/WebdriverClassicDriver.php
94 changes: 73 additions & 21 deletions src/WebdriverClassicDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
use Facebook\WebDriver\WebDriverSelect;
use JetBrains\PhpStorm\Language;

/**
* @phpstan-type TTimeouts array{script?: null|numeric, implicit?: null|numeric, page?: null|numeric, "page load"?: null|numeric, pageLoad?: null|numeric}
* @phpstan-type TCapabilities array<string, mixed>
* @phpstan-type TElementValue array<array-key, mixed>|bool|mixed|string|null
* @phpstan-type TWebDriverInstantiator callable(string $driverHost, DesiredCapabilities $capabilities): RemoteWebDriver
*/
class WebdriverClassicDriver extends CoreDriver
{
public const DEFAULT_BROWSER = WebDriverBrowserType::CHROME;
Expand Down Expand Up @@ -77,23 +83,35 @@ class WebdriverClassicDriver extends CoreDriver

private DesiredCapabilities $desiredCapabilities;

/**
* @var TTimeouts
*/
private array $timeouts = [];

private string $webDriverHost;

/**
* @var TWebDriverInstantiator
*/
private $webDriverInstantiator;

private ?string $initialWindowHandle = null;

/**
* @param string $browserName One of 'edge', 'firefox', 'chrome' or any one of {@see WebDriverBrowserType} constants.
* @param TCapabilities $desiredCapabilities
* @param TWebDriverInstantiator|null $webDriverInstantiator
*/
public function __construct(
string $browserName = self::DEFAULT_BROWSER,
array $desiredCapabilities = [],
string $webDriverHost = 'http://localhost:4444/wd/hub'
string $webDriverHost = 'http://localhost:4444/wd/hub',
?callable $webDriverInstantiator = null
) {
$this->browserName = $browserName;
$this->desiredCapabilities = $this->initCapabilities($desiredCapabilities);
$this->webDriverHost = $webDriverHost;
$this->webDriverInstantiator = $webDriverInstantiator ?? [self::class, 'instantiateWebDriver'];
}

// <editor-fold desc="Implementation">
Expand Down Expand Up @@ -262,7 +280,7 @@ public function getWindowNames(): array

public function getWindowName(): string
{
$name = (string)$this->evaluateScript('window.name');
$name = $this->getAsString($this->evaluateScript('window.name'), 'Window name');

if ($name === '') {
$name = self::W3C_WINDOW_HANDLE_PREFIX . $this->getWebDriver()->getWindowHandle();
Expand Down Expand Up @@ -299,22 +317,22 @@ public function getText(
return trim(str_replace(
["\r\n", "\r", "\n", "\xc2\xa0"],
' ',
$this->getElementDomProperty($this->findElement($xpath), 'innerText')
$this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'innerText'), 'The element\'s innerText')
));
}

public function getHtml(
#[Language('XPath')]
string $xpath
): string {
return $this->getElementDomProperty($this->findElement($xpath), 'innerHTML');
return $this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'innerHTML'), 'The element\'s innerHTML');
}

public function getOuterHtml(
#[Language('XPath')]
string $xpath
): string {
return $this->getElementDomProperty($this->findElement($xpath), 'outerHTML');
return $this->getAsString($this->getElementDomProperty($this->findElement($xpath), 'outerHTML'), 'The element\'s outerHTML');
}

public function getAttribute(
Expand All @@ -326,15 +344,20 @@ public function getAttribute(
// so we cannot use webdriver api for this. See also: https://w3c.github.io/webdriver/#dfn-get-element-attribute
$escapedName = $this->jsonEncode($name, 'get attribute', 'attribute name');
$script = "return arguments[0].getAttribute($escapedName)";
return $this->executeJsOnXpath($xpath, $script);
$result = $this->executeJsOnXpath($xpath, $script);
return $result === null ? null : $this->getAsString($result, "The element's $name attribute");
}

/**
* {@inheritdoc}
* @return TElementValue
*/
public function getValue(
#[Language('XPath')]
string $xpath
) {
$element = $this->findElement($xpath);
$widgetType = strtolower($element->getTagName() ?? '');
$widgetType = $element->getTagName();
if ($widgetType === 'input') {
$widgetType = strtolower((string)$element->getAttribute('type'));
}
Expand Down Expand Up @@ -369,13 +392,17 @@ public function getValue(
}
}

/**
* {@inheritdoc}
* @param TElementValue $value
*/
public function setValue(
#[Language('XPath')]
string $xpath,
$value
): void {
$element = $this->findElement($xpath);
$widgetType = strtolower($element->getTagName() ?? '');
$widgetType = $element->getTagName();
if ($widgetType === 'input') {
$widgetType = strtolower((string)$element->getAttribute('type'));
}
Expand All @@ -386,7 +413,7 @@ public function setValue(
if (is_array($value)) {
$this->deselectAllOptions($element);
foreach ($value as $option) {
$this->selectOptionOnElement($element, $option, true);
$this->selectOptionOnElement($element, $this->getAsString($option, 'Option value'), true);
}
return;
}
Expand Down Expand Up @@ -508,7 +535,7 @@ public function selectOption(
bool $multiple = false
): void {
$element = $this->findElement($xpath);
$tagName = strtolower($element->getTagName() ?? '');
$tagName = $element->getTagName();

if ($tagName === 'input' && strtolower((string)$element->getAttribute('type')) === 'radio') {
$this->selectRadioValue($element, $value);
Expand Down Expand Up @@ -736,7 +763,7 @@ public function getWebDriverSessionId(): ?string
/**
* Sets the timeouts to apply to the webdriver session
*
* @param array $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds
* @param TTimeouts $timeouts The session timeout settings: Array of {script, implicit, page} => time in milliseconds
* @throws DriverException
* @api
*/
Expand All @@ -751,7 +778,7 @@ public function setTimeouts(array $timeouts): void

// </editor-fold>

// <editor-fold desc="Private Utilities">
// <editor-fold desc="Extension Points">

/**
* @throws DriverException
Expand All @@ -762,7 +789,7 @@ protected function createWebDriver(): void
throw new DriverException('Base driver has already been created');
}

$this->webDriver = RemoteWebDriver::create($this->webDriverHost, $this->getDesiredCapabilities());
$this->webDriver = ($this->webDriverInstantiator)($this->webDriverHost, $this->desiredCapabilities);
}

/**
Expand All @@ -777,9 +804,13 @@ protected function getWebDriver(): RemoteWebDriver
throw new DriverException('Base driver has not been created');
}

protected function getDesiredCapabilities(): array
// </editor-fold>

// <editor-fold desc="Private Utilities">

private static function instantiateWebDriver(string $driverHost, DesiredCapabilities $capabilities): RemoteWebDriver
{
return $this->desiredCapabilities->toArray();
return RemoteWebDriver::create($driverHost, $capabilities);
}

private function getNormalisedBrowserName(): string
Expand All @@ -790,6 +821,8 @@ private function getNormalisedBrowserName(): string
/**
* Detect and assign appropriate browser capabilities
*
* @param TCapabilities $desiredCapabilities
*
* @see https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities
*/
private function initCapabilities(array $desiredCapabilities): DesiredCapabilities
Expand Down Expand Up @@ -909,12 +942,13 @@ private function charToSynOptions($char, ?string $modifier = null): string
* Executes JS on a given element - pass in a js script string and argument[0] will
* be replaced with a reference to the result of the $xpath query
*
* @param string $xpath the xpath to search with
* @param string $script the script to execute
* Example:
* ```
* $this->executeJsOnXpath($xpath, 'return argument[0].childNodes.length');
* ```
*
* @return mixed
* @throws DriverException
* @example $this->executeJsOnXpath($xpath, 'return argument[0].childNodes.length');
*/
private function executeJsOnXpath(
#[Language('XPath')]
Expand All @@ -928,11 +962,13 @@ private function executeJsOnXpath(
/**
* Executes JS on a given element - pass in a js script string and argument[0] will contain a reference to the element
*
* @param RemoteWebElement $element the webdriver element
* @param string $script the script to execute
* Example:
* ```
* $this->executeJsOnElement($element, 'return argument[0].childNodes.length');
* ```
*
* @return mixed
* @throws DriverException
* @example $this->executeJsOnXpath($xpath, 'return argument[0].childNodes.length');
*/
private function executeJsOnElement(
RemoteWebElement $element,
Expand Down Expand Up @@ -1225,5 +1261,21 @@ private function getElementDomProperty(RemoteWebElement $element, string $proper
}
}

/**
* @param mixed $value
* @throws DriverException
*/
private function getAsString($value, string $name): string
{
if (!is_scalar($value)) {
$actualType = gettype($value);
throw new DriverException(
"$name should be a string or at least a scalar value, but received `$actualType` instead"
);
}

return (string)$value;
}

// </editor-fold>
}
56 changes: 30 additions & 26 deletions tests/Custom/CapabilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,49 @@

namespace Mink\WebdriverClassicDriver\Tests\Custom;

use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverOptions;
use Facebook\WebDriver\WebDriverTimeouts;
use Mink\WebdriverClassicDriver\WebdriverClassicDriver;

/**
* @phpstan-import-type TCapabilities from WebdriverClassicDriver
*/
class CapabilityTest extends \PHPUnit\Framework\TestCase
{
/**
* @param array<string, mixed> $desiredCapabilities
* @param array<string, mixed> $expectedCapabilities
* @param TCapabilities $desiredCapabilities
* @param TCapabilities $expectedCapabilities
*
* @dataProvider capabilitiesDataProvider
*/
public function testThatCapabilitiesAreAsExpected(string $browserName, array $desiredCapabilities, array $expectedCapabilities): void
{
$driver = $this->createDriverExposingCapabilities($browserName, $desiredCapabilities);
$mockWebDriver = $this->createMock(RemoteWebDriver::class);
$mockWebDriverOptions = $this->createMock(WebDriverOptions::class);
$mockWebDriverTimeouts = $this->createMock(WebDriverTimeouts::class);
$mockWebDriver->method('manage')->willReturn($mockWebDriverOptions);
$mockWebDriverOptions->method('timeouts')->willReturn($mockWebDriverTimeouts);

$this->assertSame($expectedCapabilities, $driver->capabilities);
$actualCapabilities = null;
$driver = new WebdriverClassicDriver(
$browserName,
$desiredCapabilities,
'example.com',
function ($host, $capabilities) use (&$actualCapabilities, $mockWebDriver) {
$actualCapabilities = $capabilities->toArray();
return $mockWebDriver;
}
);

$driver->start();

$this->assertSame($expectedCapabilities, $actualCapabilities);
}

/**
* @return iterable<string, array{browserName: string, desiredCapabilities: TCapabilities, expectedCapabilities: TCapabilities}>
*/
public static function capabilitiesDataProvider(): iterable
{
yield 'unknown browser starts with default driver capabilities' => [
Expand Down Expand Up @@ -78,26 +104,4 @@ public static function capabilitiesDataProvider(): iterable
],
];
}

/**
* @param string $browserName
* @param array<string, mixed> $desiredCapabilities
* @return WebdriverClassicDriver&object{capabilities: array<string, mixed>}
*/
private function createDriverExposingCapabilities(string $browserName, array $desiredCapabilities = []): WebdriverClassicDriver
{
return new class($browserName, $desiredCapabilities) extends WebdriverClassicDriver {
/**
* @var array<string, mixed>
*/
public array $capabilities;

public function __construct(string $browserName, array $desiredCapabilities)
{
parent::__construct($browserName, $desiredCapabilities);

$this->capabilities = $this->getDesiredCapabilities();
}
};
}
}
3 changes: 3 additions & 0 deletions tests/Custom/TimeoutTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public function testDeprecatedShortPageLoadTimeoutThrowsException(string $type):
$this->driver->visit($this->pathTo('/page_load.php?sleep=2'));
}

/**
* @return iterable<string, array{type: string}>
*/
public static function deprecatedPageLoadDataProvider(): iterable
{
yield 'selenium 3 style' => ['type' => 'pageLoad'];
Expand Down
Loading
Loading