From 00b5f88d5412a373b38b82a381963d7970d11cb7 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sun, 5 Jan 2025 16:47:24 +0100 Subject: [PATCH 1/2] Implement driver initializer (#56) --- src/WebdriverClassicDriver.php | 25 ++++++++++++++++---- tests/Custom/CapabilityTest.php | 42 ++++++++++++++------------------- tests/Custom/WebDriverTest.php | 25 +++++++++----------- tests/WebDriverMockingTrait.php | 34 ++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 tests/WebDriverMockingTrait.php diff --git a/src/WebdriverClassicDriver.php b/src/WebdriverClassicDriver.php index d072288..97976be 100644 --- a/src/WebdriverClassicDriver.php +++ b/src/WebdriverClassicDriver.php @@ -31,6 +31,9 @@ use Facebook\WebDriver\WebDriverSelect; use JetBrains\PhpStorm\Language; +/** + * @phpstan-type TWebDriverInstantiator callable(string $driverHost, DesiredCapabilities $capabilities): RemoteWebDriver + */ class WebdriverClassicDriver extends CoreDriver { public const DEFAULT_BROWSER = WebDriverBrowserType::CHROME; @@ -81,19 +84,27 @@ class WebdriverClassicDriver extends CoreDriver 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 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']; } // @@ -751,7 +762,7 @@ public function setTimeouts(array $timeouts): void // - // + // /** * @throws DriverException @@ -762,7 +773,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); } /** @@ -777,9 +788,13 @@ protected function getWebDriver(): RemoteWebDriver throw new DriverException('Base driver has not been created'); } - protected function getDesiredCapabilities(): array + // + + // + + private static function instantiateWebDriver(string $driverHost, DesiredCapabilities $capabilities): RemoteWebDriver { - return $this->desiredCapabilities->toArray(); + return RemoteWebDriver::create($driverHost, $capabilities); } private function getNormalisedBrowserName(): string diff --git a/tests/Custom/CapabilityTest.php b/tests/Custom/CapabilityTest.php index f3a3082..9abc275 100644 --- a/tests/Custom/CapabilityTest.php +++ b/tests/Custom/CapabilityTest.php @@ -2,10 +2,13 @@ namespace Mink\WebdriverClassicDriver\Tests\Custom; +use Mink\WebdriverClassicDriver\Tests\WebDriverMockingTrait; use Mink\WebdriverClassicDriver\WebdriverClassicDriver; class CapabilityTest extends \PHPUnit\Framework\TestCase { + use WebDriverMockingTrait; + /** * @param array $desiredCapabilities * @param array $expectedCapabilities @@ -14,9 +17,22 @@ class CapabilityTest extends \PHPUnit\Framework\TestCase */ public function testThatCapabilitiesAreAsExpected(string $browserName, array $desiredCapabilities, array $expectedCapabilities): void { - $driver = $this->createDriverExposingCapabilities($browserName, $desiredCapabilities); + $mockWebDriver = $this->createMockWebDriver(); + + $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, $driver->capabilities); + $this->assertSame($expectedCapabilities, $actualCapabilities); } public static function capabilitiesDataProvider(): iterable @@ -78,26 +94,4 @@ public static function capabilitiesDataProvider(): iterable ], ]; } - - /** - * @param string $browserName - * @param array $desiredCapabilities - * @return WebdriverClassicDriver&object{capabilities: array} - */ - private function createDriverExposingCapabilities(string $browserName, array $desiredCapabilities = []): WebdriverClassicDriver - { - return new class($browserName, $desiredCapabilities) extends WebdriverClassicDriver { - /** - * @var array - */ - public array $capabilities; - - public function __construct(string $browserName, array $desiredCapabilities) - { - parent::__construct($browserName, $desiredCapabilities); - - $this->capabilities = $this->getDesiredCapabilities(); - } - }; - } } diff --git a/tests/Custom/WebDriverTest.php b/tests/Custom/WebDriverTest.php index 1c4ff40..6a778d4 100644 --- a/tests/Custom/WebDriverTest.php +++ b/tests/Custom/WebDriverTest.php @@ -3,10 +3,13 @@ namespace Mink\WebdriverClassicDriver\Tests\Custom; use Behat\Mink\Exception\DriverException; +use Mink\WebdriverClassicDriver\Tests\WebDriverMockingTrait; use Mink\WebdriverClassicDriver\WebdriverClassicDriver; class WebDriverTest extends TestCase { + use WebDriverMockingTrait; + public function testDriverMustBeStartedBeforeUse(): void { $this->expectException(DriverException::class); @@ -35,11 +38,9 @@ public function testStartedDriverCannotBeSubsequentlyStarted(): void public function testDriverCatchesUpstreamErrorsDuringStart(): void { - $driver = $this->createPartialMock(WebdriverClassicDriver::class, ['createWebDriver', 'getWebDriver']); - $driver - ->expects($this->once()) - ->method('createWebDriver') - ->willThrowException(new \RuntimeException('An upstream error')); + $driver = new WebdriverClassicDriver('fake browser', [], 'example.com', function () { + throw new \RuntimeException('An upstream error'); + }); $this->expectException(DriverException::class); $this->expectExceptionMessage('Could not start driver: An upstream error'); @@ -49,15 +50,11 @@ public function testDriverCatchesUpstreamErrorsDuringStart(): void public function testDriverCatchesUpstreamErrorsDuringStop(): void { - $driver = $this->createPartialMock(WebdriverClassicDriver::class, ['createWebDriver', 'isStarted', 'getWebDriver']); - $driver - ->expects($this->once()) - ->method('isStarted') - ->willReturn(true); - $driver - ->expects($this->once()) - ->method('getWebDriver') - ->willThrowException(new \RuntimeException('An upstream error')); + $mockWebDriver = $this->createMockWebDriver(); + $mockWebDriver->method('quit')->willThrowException(new \RuntimeException('An upstream error')); + $driver = new WebdriverClassicDriver('fake browser', [], 'example.com', fn() => $mockWebDriver); + + $driver->start(); $this->expectException(DriverException::class); $this->expectExceptionMessage('Could not close connection: An upstream error'); diff --git a/tests/WebDriverMockingTrait.php b/tests/WebDriverMockingTrait.php new file mode 100644 index 0000000..3bce02e --- /dev/null +++ b/tests/WebDriverMockingTrait.php @@ -0,0 +1,34 @@ + $class + * @return T&MockObject + */ + abstract function createMock(string $class): object; + + /** + * @return RemoteWebDriver&MockObject + */ + private function createMockWebDriver(): RemoteWebDriver + { + $mockWebDriverTimeouts = $this->createMock(WebDriverTimeouts::class); + + $mockWebDriverOptions = $this->createMock(WebDriverOptions::class); + $mockWebDriverOptions->method('timeouts')->willReturn($mockWebDriverTimeouts); + + $mockWebDriver = $this->createMock(RemoteWebDriver::class); + $mockWebDriver->method('manage')->willReturn($mockWebDriverOptions); + + return $mockWebDriver; + } +} From 0d75b54b197938f7884b7db2d6b621d3e1deb78e Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sun, 5 Jan 2025 16:54:36 +0100 Subject: [PATCH 2/2] Fix various phpstan warnings (#57) --- .editorconfig | 3 +++ phpstan.dist.neon | 37 ++++++++++++++++++++++++--------- src/WebdriverClassicDriver.php | 25 ++++++++++++++++++---- tests/Custom/CapabilityTest.php | 10 +++++++-- tests/Custom/TimeoutTest.php | 3 +++ 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9cd5d20..879a1c1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ indent_size = 2 [*.php] ij_php_align_multiline_parameters = false + +[*.neon] +indent_style = tab diff --git a/phpstan.dist.neon b/phpstan.dist.neon index db774f4..7b42960 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -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: 8 + 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 diff --git a/src/WebdriverClassicDriver.php b/src/WebdriverClassicDriver.php index 97976be..da24eae 100644 --- a/src/WebdriverClassicDriver.php +++ b/src/WebdriverClassicDriver.php @@ -32,6 +32,9 @@ 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 + * @phpstan-type TElementValue array|bool|mixed|string|null * @phpstan-type TWebDriverInstantiator callable(string $driverHost, DesiredCapabilities $capabilities): RemoteWebDriver */ class WebdriverClassicDriver extends CoreDriver @@ -80,6 +83,9 @@ class WebdriverClassicDriver extends CoreDriver private DesiredCapabilities $desiredCapabilities; + /** + * @var TTimeouts + */ private array $timeouts = []; private string $webDriverHost; @@ -93,6 +99,7 @@ class WebdriverClassicDriver extends CoreDriver /** * @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( @@ -340,12 +347,16 @@ public function getAttribute( return $this->executeJsOnXpath($xpath, $script); } + /** + * {@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')); } @@ -380,13 +391,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')); } @@ -519,7 +534,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); @@ -747,7 +762,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 */ @@ -805,6 +820,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 diff --git a/tests/Custom/CapabilityTest.php b/tests/Custom/CapabilityTest.php index 9abc275..ce129d0 100644 --- a/tests/Custom/CapabilityTest.php +++ b/tests/Custom/CapabilityTest.php @@ -5,13 +5,16 @@ use Mink\WebdriverClassicDriver\Tests\WebDriverMockingTrait; use Mink\WebdriverClassicDriver\WebdriverClassicDriver; +/** + * @phpstan-import-type TCapabilities from WebdriverClassicDriver + */ class CapabilityTest extends \PHPUnit\Framework\TestCase { use WebDriverMockingTrait; /** - * @param array $desiredCapabilities - * @param array $expectedCapabilities + * @param TCapabilities $desiredCapabilities + * @param TCapabilities $expectedCapabilities * * @dataProvider capabilitiesDataProvider */ @@ -35,6 +38,9 @@ function ($host, $capabilities) use (&$actualCapabilities, $mockWebDriver) { $this->assertSame($expectedCapabilities, $actualCapabilities); } + /** + * @return iterable + */ public static function capabilitiesDataProvider(): iterable { yield 'unknown browser starts with default driver capabilities' => [ diff --git a/tests/Custom/TimeoutTest.php b/tests/Custom/TimeoutTest.php index b8c49e9..9fe3ae6 100644 --- a/tests/Custom/TimeoutTest.php +++ b/tests/Custom/TimeoutTest.php @@ -65,6 +65,9 @@ public function testDeprecatedShortPageLoadTimeoutThrowsException(string $type): $this->driver->visit($this->pathTo('/page_load.php?sleep=2')); } + /** + * @return iterable + */ public static function deprecatedPageLoadDataProvider(): iterable { yield 'selenium 3 style' => ['type' => 'pageLoad'];