diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01909e6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +tab_width = 4 +trim_trailing_whitespace = true + +[.github/workflows/*.yml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 5091d75..1c61dac 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,8 @@ +/.editorconfig export-ignore /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore /phpunit.xml.dist export-ignore /phpunit.http_client.xml export-ignore +/phpstan*.neon export-ignore /tests export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2738dba..10b42c5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,29 @@ defaults: shell: bash jobs: + check_composer: + name: Check composer.json + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: '8.2' + - run: composer validate --strict --no-check-lock + + static_analysis: + name: Static analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: '8.2' + - name: Install dependencies + run: composer update --ansi --no-progress --prefer-dist --no-interaction + - run: vendor/bin/phpstan analyze tests: name: Tests on PHP ${{ matrix.php }} with ${{ matrix.implementation }}${{ matrix.name_suffix }} diff --git a/composer.json b/composer.json index 8ad1513..9ca8614 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "require": { "php": ">=7.2", + "ext-dom": "*", "behat/mink": "^1.9.0@dev", "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", "symfony/dom-crawler": "^4.4 || ^5.0 || ^6.0" @@ -23,11 +24,13 @@ "require-dev": { "mink/driver-testsuite": "dev-master", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", "symfony/error-handler": "^4.4 || ^5.0 || ^6.0", "symfony/http-client": "^4.4 || ^5.0 || ^6.0", "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", "symfony/mime": "^4.4 || ^5.0 || ^6.0", - "phpunit/phpunit": "^8.5 || ^9.5", "yoast/phpunit-polyfills": "^1.0" }, diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..f7cbf4c --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,12 @@ +parameters: + level: 8 + paths: + - src + - tests + checkMissingIterableValueType: false + ignoreErrors: + - '#^Method Behat\\Mink\\Tests\\Driver\\Custom\\[^:]+Test(Case)?\:\:test\w*\(\) has no return type specified\.$#' + +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon diff --git a/src/BrowserKitDriver.php b/src/BrowserKitDriver.php index 9f887e1..a216dff 100644 --- a/src/BrowserKitDriver.php +++ b/src/BrowserKitDriver.php @@ -13,7 +13,6 @@ use Behat\Mink\Exception\DriverException; use Behat\Mink\Exception\UnsupportedDriverActionException; use Symfony\Component\BrowserKit\AbstractBrowser; -use Symfony\Component\BrowserKit\Client; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\Exception\BadMethodCallException; use Symfony\Component\BrowserKit\Response; @@ -32,13 +31,22 @@ */ class BrowserKitDriver extends CoreDriver { + /** + * @var AbstractBrowser + */ private $client; /** - * @var Form[] + * @var array */ private $forms = array(); + /** + * @var array + */ private $serverParameters = array(); + /** + * @var bool + */ private $started = false; /** @@ -46,13 +54,17 @@ class BrowserKitDriver extends CoreDriver * * @param string|null $baseUrl Base URL for HttpKernel clients */ - public function __construct(AbstractBrowser $client, $baseUrl = null) + public function __construct(AbstractBrowser $client, ?string $baseUrl = null) { $this->client = $client; $this->client->followRedirects(true); if ($baseUrl !== null && $client instanceof HttpKernelBrowser) { - $client->setServerParameter('SCRIPT_FILENAME', parse_url($baseUrl, PHP_URL_PATH)); + $basePath = parse_url($baseUrl, PHP_URL_PATH); + + if (\is_string($basePath)) { + $client->setServerParameter('SCRIPT_FILENAME', $basePath); + } } } @@ -219,7 +231,7 @@ public function setCookie($name, $value = null) * * @param string $name Cookie name. */ - private function deleteCookie($name) + private function deleteCookie(string $name): void { $path = $this->getCookiePath(); $jar = $this->client->getCookieJar(); @@ -235,13 +247,15 @@ private function deleteCookie($name) /** * Returns current cookie path. - * - * @return string */ - private function getCookiePath() + private function getCookiePath(): string { $path = parse_url($this->getCurrentUrl(), PHP_URL_PATH); + if ($path === null || $path === false || $path === '') { + $path = '/'; + } + if ('\\' === DIRECTORY_SEPARATOR) { $path = str_replace('\\', '/', $path); } @@ -394,7 +408,18 @@ public function getValue($xpath) */ public function setValue($xpath, $value) { - $this->getFormField($xpath)->setValue($value); + $field = $this->getFormField($xpath); + + if ($field instanceof ChoiceFormField) { + $field->setValue($value); + return; + } + + if (!\is_string($value) && null !== $value) { + throw new DriverException('Textual form fields don\'t support array or boolean values'); + } + + $field->setValue($value); } /** @@ -554,6 +579,7 @@ protected function prepareUrl($url) * @return FormField * * @throws DriverException + * @throws \InvalidArgumentException when the field does not exist in the BrowserKit form */ protected function getFormField($xpath) { @@ -568,7 +594,11 @@ protected function getFormField($xpath) } if (is_array($this->forms[$formId][$fieldName])) { - return $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)]; + $positionField = $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)]; + + \assert($positionField instanceof FormField); + + return $positionField; } return $this->forms[$formId][$fieldName]; @@ -605,6 +635,7 @@ private function getFormNode(\DOMElement $element) { if ($element->hasAttribute('form')) { $formId = $element->getAttribute('form'); + \assert($element->ownerDocument !== null); $formNode = $element->ownerDocument->getElementById($formId); if (null === $formNode || 'form' !== $formNode->nodeName) { @@ -623,6 +654,8 @@ private function getFormNode(\DOMElement $element) } } while ('form' !== $formNode->nodeName); + \assert($formNode instanceof \DOMElement); + return $formNode; } @@ -633,11 +666,9 @@ private function getFormNode(\DOMElement $element) * When multiple fields have the same name (checkboxes for instance), it will return * an array of elements in the order they appear in the DOM. * - * @param \DOMElement $fieldNode - * - * @return integer + * @throws DriverException */ - private function getFieldPosition(\DOMElement $fieldNode) + private function getFieldPosition(\DOMElement $fieldNode): int { $elements = $this->getCrawler()->filterXPath('//*[@name=\''.$fieldNode->getAttribute('name').'\']'); @@ -655,7 +686,7 @@ private function getFieldPosition(\DOMElement $fieldNode) return 0; } - private function submit(Form $form) + private function submit(Form $form): void { $formId = $this->getFormNodeId($form->getFormNode()); @@ -675,21 +706,14 @@ private function submit(Form $form) $this->forms = array(); } - private function resetForm(\DOMElement $fieldNode) + private function resetForm(\DOMElement $fieldNode): void { $formNode = $this->getFormNode($fieldNode); $formId = $this->getFormNodeId($formNode); unset($this->forms[$formId]); } - /** - * Determines if a node can submit a form. - * - * @param \DOMElement $node Node. - * - * @return boolean - */ - private function canSubmitForm(\DOMElement $node) + private function canSubmitForm(\DOMElement $node): bool { $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null; @@ -700,14 +724,7 @@ private function canSubmitForm(\DOMElement $node) return 'button' === $node->nodeName && (null === $type || 'submit' === $type); } - /** - * Determines if a node can reset a form. - * - * @param \DOMElement $node Node. - * - * @return boolean - */ - private function canResetForm(\DOMElement $node) + private function canResetForm(\DOMElement $node): bool { $type = $node->hasAttribute('type') ? $node->getAttribute('type') : null; @@ -754,7 +771,7 @@ private function getOptionValue(\DOMElement $option) * @param Form $to merging target * @param Form $from merging source */ - private function mergeForms(Form $to, Form $from) + private function mergeForms(Form $to, Form $from): void { foreach ($from->all() as $name => $field) { $fieldReflection = new \ReflectionObject($field); @@ -768,7 +785,11 @@ private function mergeForms(Form $to, Form $from) in_array($nodeReflection->getValue($field)->getAttribute('type'), array('submit', 'button', 'image'), true); if (!$isIgnoredField) { - $valueReflection->setValue($to[$name], $valueReflection->getValue($field)); + $targetField = $to[$name]; + + \assert($targetField instanceof FormField); + + $valueReflection->setValue($targetField, $valueReflection->getValue($field)); } } } @@ -776,17 +797,15 @@ private function mergeForms(Form $to, Form $from) /** * Returns DOMElement from crawler instance. * - * @param Crawler $crawler - * - * @return \DOMElement - * * @throws DriverException when the node does not exist */ - private function getCrawlerNode(Crawler $crawler) + private function getCrawlerNode(Crawler $crawler): \DOMElement { $node = $crawler->getNode(0); if (null !== $node) { + \assert($node instanceof \DOMElement); + return $node; } diff --git a/tests/AbstractBrowserKitConfig.php b/tests/AbstractBrowserKitConfig.php index 1003eed..6f096b0 100644 --- a/tests/AbstractBrowserKitConfig.php +++ b/tests/AbstractBrowserKitConfig.php @@ -7,6 +7,13 @@ abstract class AbstractBrowserKitConfig extends AbstractConfig { + final public function __construct() + { + } + + /** + * @return static + */ public static function getInstance() { return new static(); @@ -25,7 +32,7 @@ public function skipMessage($testCase, $test): ?string return parent::skipMessage($testCase, $test); } - protected function supportsJs() + protected function supportsJs(): bool { return false; } diff --git a/tests/Custom/ErrorHandlingTest.php b/tests/Custom/ErrorHandlingTest.php index aa6dbad..19ba56d 100644 --- a/tests/Custom/ErrorHandlingTest.php +++ b/tests/Custom/ErrorHandlingTest.php @@ -20,7 +20,7 @@ class ErrorHandlingTest extends TestCase /** * @before */ - protected function prepareClient() + protected function prepareClient(): void { $this->client = new TestClient(); } @@ -154,7 +154,7 @@ public function testClickOnUnsupportedElement() $driver->click('//div'); } - private function getDriver() + private function getDriver(): BrowserKitDriver { return new BrowserKitDriver($this->client); } @@ -162,19 +162,19 @@ private function getDriver() class TestClient extends AbstractBrowser { + /** + * @var Response|null + */ protected $nextResponse = null; - protected $nextScript = null; - public function setNextResponse(Response $response) + public function setNextResponse(Response $response): void { $this->nextResponse = $response; } - public function setNextScript($script) - { - $this->nextScript = $script; - } - + /** + * @param object $request + */ protected function doRequest($request): object { if (null === $this->nextResponse) { diff --git a/tests/HttpClientBrowserKitConfig.php b/tests/HttpClientBrowserKitConfig.php index 900d0c6..4b6576f 100644 --- a/tests/HttpClientBrowserKitConfig.php +++ b/tests/HttpClientBrowserKitConfig.php @@ -3,11 +3,12 @@ namespace Behat\Mink\Tests\Driver; use Behat\Mink\Driver\BrowserKitDriver; +use Behat\Mink\Driver\DriverInterface; use Symfony\Component\BrowserKit\HttpBrowser; class HttpClientBrowserKitConfig extends AbstractBrowserKitConfig { - public function createDriver() + public function createDriver(): DriverInterface { return new BrowserKitDriver(new HttpBrowser()); } diff --git a/tests/HttpKernelBrowserKitConfig.php b/tests/HttpKernelBrowserKitConfig.php index 9b89a3d..13e2e33 100644 --- a/tests/HttpKernelBrowserKitConfig.php +++ b/tests/HttpKernelBrowserKitConfig.php @@ -3,25 +3,20 @@ namespace Behat\Mink\Tests\Driver; use Behat\Mink\Driver\BrowserKitDriver; +use Behat\Mink\Driver\DriverInterface; use Behat\Mink\Tests\Driver\Util\FixturesKernel; use Symfony\Component\HttpKernel\HttpKernelBrowser; class HttpKernelBrowserKitConfig extends AbstractBrowserKitConfig { - /** - * {@inheritdoc} - */ - public function createDriver() + public function createDriver(): DriverInterface { $client = new HttpKernelBrowser(new FixturesKernel()); return new BrowserKitDriver($client); } - /** - * {@inheritdoc} - */ - public function getWebFixturesUrl() + public function getWebFixturesUrl(): string { return 'http://localhost'; }