diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 98% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 1c0658b..c581be8 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -44,7 +44,7 @@ This project uses [the PSR-2 coding standards](http://www.php-fig.org/psr/psr-2/ [PHPUnit](https://phpunit.de/) is included as a development dependency, and should be run regularly. When submitting changes, please be sure to add or update unit tests accordingly. You may run unit tests at any time by running: ```bash -$ ./vendor/bin/phpunit +$ composer test ``` #### Code coverage diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml new file mode 100644 index 0000000..6eee129 --- /dev/null +++ b/.github/workflows/code-coverage.yml @@ -0,0 +1,35 @@ +name: Code Coverage + +on: + pull_request: + push: + branches: + - develop + - main + +jobs: + coverage: + name: Report code coverage + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: xdebug + + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + + - name: Run test suite + run: vendor/bin/simple-phpunit --coverage-text --coverage-clover=tests/coverage + + - name: Publish to Coveralls + uses: coverallsapp/github-action@v2 + with: + files: tests/coverage + format: clover + fail-on-error: false diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..68d77bc --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,23 @@ +name: Coding Standards + +on: [pull_request] + +jobs: + phpcs: + name: PHP_CodeSniffer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + + - name: Run test suite + run: composer coding-standards diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml new file mode 100644 index 0000000..745f15f --- /dev/null +++ b/.github/workflows/static-code-analysis.yml @@ -0,0 +1,28 @@ +name: Static Code Analysis + +on: [pull_request] + +jobs: + phpcs: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + + # PHPUnit Bridge won't install a version of PHPUnit by default, but this will trick + # it into doing so. + - name: Install PHPUnit + run: composer test -- --version + + - name: Run PHPStan + run: composer static-analysis diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 84df900..0ec9256 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -8,10 +8,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php-version: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -19,6 +19,9 @@ jobs: php-version: ${{ matrix.php-version }} coverage: none + - name: Remove PHPStan as a dependency + run: composer remove --dev phpstan/phpstan + - name: Install Composer dependencies uses: ramsey/composer-install@v2 diff --git a/.gitignore b/.gitignore index bb4f4cb..415274e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ -tests/coverage -vendor +*.DS_Store .phpunit.result.cache .vscode +phpcs.xml +phpstan.neon +phpunit.xml +tests/coverage +vendor # The composer.lock file is not needed, as this is a library whose dependencies # will depend on the version of PHP being used. diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..df2db46 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,24 @@ + + + Coding standards for PHPUnit Markup Assertions + + + + + + + . + */vendor/* + + + + + + + tests/* + + + + + + diff --git a/README.md b/README.md index 861f893..8915add 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # PHPUnit Markup Assertions ![Build Status](https://github.com/stevegrunwell/phpunit-markup-assertions/workflows/Unit%20Tests/badge.svg) -[![GitHub release](https://img.shields.io/github/release/stevegrunwell/phpunit-markup-assertions.svg)](https://github.com/stevegrunwell/phpunit-markup-assertions/releases) +[![Code Coverage](https://coveralls.io/repos/github/stevegrunwell/phpunit-markup-assertions/badge.svg?branch=develop)](https://coveralls.io/github/stevegrunwell/phpunit-markup-assertions?branch=develop) +[![GitHub Release](https://img.shields.io/github/release/stevegrunwell/phpunit-markup-assertions.svg)](https://github.com/stevegrunwell/phpunit-markup-assertions/releases) This library introduces the `MarkupAssertionsTrait` trait for use in [PHPUnit](https://phpunit.de) tests. diff --git a/composer.json b/composer.json index a757aca..f3a4abf 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,16 @@ "source": "https://github.com/stevegrunwell/phpunit-markup-assertions/" }, "require": { - "php": "^5.6 || ^7.0 || ^8.0", - "laminas/laminas-dom": "~2.7.2 || ^2.8" + "php": "^7.1 || ^8.0", + "symfony/css-selector": "^4.4|^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^4.4|^5.4|^6.0|^7.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.2 || ^6.2" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^2.1", + "squizlabs/php_codesniffer": "^3.7", + "symfony/phpunit-bridge": "^5.2 || ^6.2 || ^7.0" }, "autoload": { "psr-4": { @@ -32,19 +37,38 @@ } }, "scripts": { + "coding-standards": [ + "phpcs" + ], + "static-analysis": [ + "phpstan analyse" + ], "test": [ "simple-phpunit --testdox" ], "test-coverage": [ - "phpdbg -qrr -d memory_limit=-1 ./vendor/bin/simple-phpunit --coverage-html=tests/coverage --colors=always" + "XDEBUG_MODE=coverage ./vendor/bin/simple-phpunit --coverage-html=tests/coverage --colors=always" ] }, "scripts-descriptions": { + "coding-standards": "Check coding standards.", + "static-analysis": "Run static code analysis", "test": "Run all test suites.", "test-coverage": "Generate code coverage reports in tests/coverage." }, "config": { "preferred-install": "dist", - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "archive": { + "exclude": [ + "_config.yml", + ".*", + "phpunit.*", + "tests" + ] } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..3cbe92e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,15 @@ +parameters: + level: 10 + + # Code to be analyzed + paths: + - src + - tests + + # Don't worry about the coverage reports + excludePaths: + - tests/coverage (?) + + # PHPUnit Bridge puts the PHPUnit source in an unconventional location + scanDirectories: + - vendor/bin/.phpunit/phpunit diff --git a/src/MarkupAssertionsTrait.php b/src/MarkupAssertionsTrait.php index 3e5cf1f..ea94aea 100644 --- a/src/MarkupAssertionsTrait.php +++ b/src/MarkupAssertionsTrait.php @@ -1,4 +1,5 @@ $attributes An array of HTML attributes that should be found + * on the element. + * @param string $markup The output that should contain an element with the + * provided $attributes. + * @param string $message A message to display if the assertion fails. * * @return void */ @@ -94,10 +96,11 @@ public function assertHasElementWithAttributes($attributes = [], $markup = '', $ * * @since 1.0.0 * - * @param array $attributes An array of HTML attributes that should be found on the element. - * @param string $markup The output that should not contain an element with the - * provided $attributes. - * @param string $message A message to display if the assertion fails. + * @param array $attributes An array of HTML attributes that should be found + * on the element. + * @param string $markup The output that should not contain an element with + * the provided $attributes. + * @param string $message A message to display if the assertion fails. * * @return void */ @@ -124,11 +127,7 @@ public function assertNotHasElementWithAttributes($attributes = [], $markup = '' */ public function assertElementContains($contents, $selector = '', $markup = '', $message = '') { - $method = method_exists($this, 'assertStringContainsString') - ? 'assertStringContainsString' - : 'assertContains'; - - $this->$method( + $this->assertStringContainsString( $contents, $this->getInnerHtmlOfMatchedElements($markup, $selector), $message @@ -149,11 +148,7 @@ public function assertElementContains($contents, $selector = '', $markup = '', $ */ public function assertElementNotContains($contents, $selector = '', $markup = '', $message = '') { - $method = method_exists($this, 'assertStringNotContainsString') - ? 'assertStringNotContainsString' - : 'assertNotContains'; - - $this->$method( + $this->assertStringNotContainsString( $contents, $this->getInnerHtmlOfMatchedElements($markup, $selector), $message @@ -174,9 +169,10 @@ public function assertElementNotContains($contents, $selector = '', $markup = '' */ public function assertElementRegExp($regexp, $selector = '', $markup = '', $message = '') { + // @phpstan-ignore function.alreadyNarrowedType (Introduced in PHPUnit 9.x, PHP 7.3+) $method = method_exists($this, 'assertMatchesRegularExpression') ? 'assertMatchesRegularExpression' - : 'assertRegExp'; + : 'assertRegExp'; // @codeCoverageIgnore $this->$method( $regexp, @@ -199,9 +195,10 @@ public function assertElementRegExp($regexp, $selector = '', $markup = '', $mess */ public function assertElementNotRegExp($regexp, $selector = '', $markup = '', $message = '') { + // @phpstan-ignore function.alreadyNarrowedType (Introduced in PHPUnit 9.x, PHP 7.3+) $method = method_exists($this, 'assertDoesNotMatchRegularExpression') ? 'assertDoesNotMatchRegularExpression' - : 'assertNotRegExp'; + : 'assertNotRegExp'; // @codeCoverageIgnore $this->$method( $regexp, @@ -218,15 +215,13 @@ public function assertElementNotRegExp($regexp, $selector = '', $markup = '', $m * @param string $markup The HTML for the DOMDocument. * @param string $query The DOM selector query. * - * @return \Laminas\Dom\Document\NodeList + * @return Crawler */ - protected function executeDomQuery($markup, $query) + private function executeDomQuery($markup, $query) { - return Query::execute( - $query, - new Document('' . $markup, Document::DOC_HTML, 'UTF-8'), - Query::TYPE_CSS - ); + $dom = new Crawler($markup); + + return $dom->filter($query); } /** @@ -236,11 +231,11 @@ protected function executeDomQuery($markup, $query) * * @throws RiskyTestError When the $attributes array is empty. * - * @param array $attributes HTML attributes and their values. + * @param array $attributes HTML attributes and their values. * * @return string A XPath attribute query selector. */ - protected function flattenAttributeArray(array $attributes) + private function flattenAttributeArray(array $attributes) { if (empty($attributes)) { throw new RiskyTestError('Attributes array is empty.'); @@ -248,10 +243,10 @@ protected function flattenAttributeArray(array $attributes) array_walk($attributes, function (&$value, $key) { // Boolean attributes. - if (null === $value) { + if (empty($value)) { $value = sprintf('[%s]', $key); } else { - $value = sprintf('[%s="%s"]', $key, htmlspecialchars($value)); + $value = sprintf('[%s="%s"]', $key, htmlspecialchars((string) $value)); } }); @@ -268,17 +263,21 @@ protected function flattenAttributeArray(array $attributes) * * @return string The concatenated innerHTML of any matched selectors. */ - protected function getInnerHtmlOfMatchedElements($markup, $query) + private function getInnerHtmlOfMatchedElements($markup, $query) { $results = $this->executeDomQuery($markup, $query); $contents = []; // Loop through results and collect their innerHTML values. foreach ($results as $result) { - $document = new DOMDocument(); + if (!isset($result->firstChild)) { + continue; + } + + $document = new \DOMDocument(); $document->appendChild($document->importNode($result->firstChild, true)); - $contents[] = trim(html_entity_decode($document->saveHTML())); + $contents[] = trim(html_entity_decode((string) $document->saveHTML())); } return implode(PHP_EOL, $contents); diff --git a/tests/MarkupAssertionsTraitTest.php b/tests/MarkupAssertionsTraitTest.php index c16aae4..2666c8f 100644 --- a/tests/MarkupAssertionsTraitTest.php +++ b/tests/MarkupAssertionsTraitTest.php @@ -19,7 +19,7 @@ class MarkupAssertionsTraitTest extends TestCase * @testdox assertContainsSelector() should find matching selectors * @dataProvider provideSelectorVariants */ - public function assertContainsSelector_should_find_matching_selectors($selector) + public function assertContainsSelector_should_find_matching_selectors(string $selector): void { $this->assertContainsSelector( $selector, @@ -31,7 +31,7 @@ public function assertContainsSelector_should_find_matching_selectors($selector) * @test * @testdox assertContainsSelector() should pick up multiple instances of a selector */ - public function assertContainsSelector_should_pick_up_multiple_instances() + public function assertContainsSelector_should_pick_up_multiple_instances(): void { $this->assertContainsSelector( 'a', @@ -44,8 +44,9 @@ public function assertContainsSelector_should_pick_up_multiple_instances() * @testdox assertNotContainsSelector() should verify that the given selector does not exist * @dataProvider provideSelectorVariants */ - public function assertNotContainsSelector_should_verify_that_the_given_selector_does_not_exist($selector) - { + public function assertNotContainsSelector_should_verify_that_the_given_selector_does_not_exist( + string $selector + ): void { $this->assertNotContainsSelector( $selector, '

This element has little to do with the link.

' @@ -56,7 +57,7 @@ public function assertNotContainsSelector_should_verify_that_the_given_selector_ * @test * @testdox assertSelectorCount() should count the instances of a selector */ - public function assertSelectorCount_should_count_the_number_of_instances() + public function assertSelectorCount_should_count_the_number_of_instances(): void { $this->assertSelectorCount( 3, @@ -69,7 +70,7 @@ public function assertSelectorCount_should_count_the_number_of_instances() * @test * @testdox assertHasElementWithAttributes() should find an element with the given attributes */ - public function assertHasElementWithAttributes_should_find_elements_with_matching_attributes() + public function assertHasElementWithAttributes_should_find_elements_with_matching_attributes(): void { $this->assertHasElementWithAttributes( [ @@ -85,7 +86,7 @@ public function assertHasElementWithAttributes_should_find_elements_with_matchin * @testdox assertHasElementWithAttributes() should be able to parse spaces in attribute values * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/13 */ - public function assertHasElementWithAttributes_should_be_able_to_handle_spaces() + public function assertHasElementWithAttributes_should_be_able_to_handle_spaces(): void { $this->assertHasElementWithAttributes( [ @@ -99,7 +100,7 @@ public function assertHasElementWithAttributes_should_be_able_to_handle_spaces() * @test * @testdox assertNotHasElementWithAttributes() should ensure no element has the provided attributes */ - public function assertNotHasElementWithAttributes_should_find_no_elements_with_matching_attributes() + public function assertNotHasElementWithAttributes_should_find_no_elements_with_matching_attributes(): void { $this->assertNotHasElementWithAttributes( [ @@ -114,7 +115,7 @@ public function assertNotHasElementWithAttributes_should_find_no_elements_with_m * @test * @testdox assertElementContains() should be able to search for a selector */ - public function assertElementContains_can_match_a_selector() + public function assertElementContains_can_match_a_selector(): void { $this->assertElementContains( 'ipsum', @@ -127,7 +128,7 @@ public function assertElementContains_can_match_a_selector() * @test * @testdox assertElementContains() should be able to chain multiple selectors */ - public function assertElementContains_can_chain_multiple_selectors() + public function assertElementContains_can_chain_multiple_selectors(): void { $this->assertElementContains( 'ipsum', @@ -140,7 +141,7 @@ public function assertElementContains_can_chain_multiple_selectors() * @test * @testdox assertElementContains() should scope text to the selected element */ - public function assertElementContains_should_scope_matches_to_selector() + public function assertElementContains_should_scope_matches_to_selector(): void { $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('The #main div does not contain the string "ipsum".'); @@ -159,7 +160,7 @@ public function assertElementContains_should_scope_matches_to_selector() * @dataProvider provideGreetingsInDifferentLanguages * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/31 */ - public function assertElementContains_should_handle_various_character_sets($greeting) + public function assertElementContains_should_handle_various_character_sets(string $greeting): void { $this->assertElementContains( $greeting, @@ -172,7 +173,7 @@ public function assertElementContains_should_handle_various_character_sets($gree * @test * @testdox assertElementNotContains() should be able to search for a selector */ - public function assertElementNotContains_can_match_a_selector() + public function assertElementNotContains_can_match_a_selector(): void { $this->assertElementNotContains( 'ipsum', @@ -187,7 +188,7 @@ public function assertElementNotContains_can_match_a_selector() * @dataProvider provideGreetingsInDifferentLanguages * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/31 */ - public function assertElementNotContains_should_handle_various_character_sets($greeting) + public function assertElementNotContains_should_handle_various_character_sets(string $greeting): void { $this->assertElementNotContains( $greeting, @@ -200,7 +201,7 @@ public function assertElementNotContains_should_handle_various_character_sets($g * @test * @testdox assertElementRegExp() should use regular expression matching */ - public function assertElementRegExp_should_use_regular_expression_matching() + public function assertElementRegExp_should_use_regular_expression_matching(): void { $this->assertElementRegExp( '/[A-Z0-9-]+/', @@ -213,7 +214,7 @@ public function assertElementRegExp_should_use_regular_expression_matching() * @test * @testdox assertElementRegExp() should be able to search for nested contents */ - public function assertElementRegExp_should_be_able_to_match_nested_contents() + public function assertElementRegExp_should_be_able_to_match_nested_contents(): void { $this->assertElementRegExp( '/[A-Z]+/', @@ -226,7 +227,7 @@ public function assertElementRegExp_should_be_able_to_match_nested_contents() * @test * @testdox assertElementNotRegExp() should use regular expression matching */ - public function testAssertElementNotRegExp() + public function testAssertElementNotRegExp(): void { $this->assertElementNotRegExp( '/[0-9-]+/', @@ -240,8 +241,10 @@ public function testAssertElementNotRegExp() * @test * @testdox flattenAttributeArray() should flatten an array of attributes * @dataProvider provideAttributes + * + * @param array $attributes */ - public function flattenArrayAttribute_should_flatten_arrays_of_attributes($attributes, $expected) + public function flattenArrayAttribute_should_flatten_arrays_of_attributes(array $attributes, string $expected): void { $method = new \ReflectionMethod($this, 'flattenAttributeArray'); $method->setAccessible(true); @@ -254,7 +257,7 @@ public function flattenArrayAttribute_should_flatten_arrays_of_attributes($attri * @testdox flattenAttributeArray() should throw a RiskyTestError if the array is empty * @dataProvider provideAttributes */ - public function flattenAttributeArray_should_throw_a_RiskyTestError_if_given_an_empty_array() + public function flattenAttributeArray_should_throw_a_RiskyTestError_if_given_an_empty_array(): void { $this->expectException(RiskyTestError::class); @@ -268,7 +271,11 @@ public function flattenAttributeArray_should_throw_a_RiskyTestError_if_given_an_ * @testdox getInnerHtmlOfMatchedElements() should retrieve the inner HTML * @dataProvider provideInnerHtml */ - public function getInnerHtmlOfMatchedElements_should_retrieve_the_inner_HTML($markup, $selector, $expected) { + public function getInnerHtmlOfMatchedElements_should_retrieve_the_inner_HTML( + string $markup, + string $selector, + string $expected + ): void { $method = new \ReflectionMethod($this, 'getInnerHtmlOfMatchedElements'); $method->setAccessible(true); @@ -277,8 +284,10 @@ public function getInnerHtmlOfMatchedElements_should_retrieve_the_inner_HTML($ma /** * Data provider for testFlattenAttributeArray(). + * + * @return array,string}> */ - public function provideAttributes() + public function provideAttributes(): array { return [ 'Single attribute' => [ @@ -320,7 +329,7 @@ public function provideAttributes() * * @return array> */ - public function provideInnerHtml() + public function provideInnerHtml(): array { return [ 'A single match' => [ @@ -346,7 +355,7 @@ public function provideInnerHtml() * * @return array> */ - public function provideSelectorVariants() + public function provideSelectorVariants(): array { return [ 'Simple tag name' => ['a'], @@ -364,7 +373,7 @@ public function provideSelectorVariants() * * @return array> */ - public function provideGreetingsInDifferentLanguages() + public function provideGreetingsInDifferentLanguages(): array { return [ 'Arabic' => ['مرحبا!'],