Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove parsing, selector logic from the trait; introduce custom constraints #46

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 .github/workflows/static-code-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ jobs:
- name: Install Composer dependencies
uses: ramsey/composer-install@v2

- name: Install PHPUnit
run: composer test -- --version

- name: Run PHPStan
run: composer static-analysis
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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', '8.3']
php-version: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@

<!-- Ensure we're compatible with PHP 5.6+ -->
<rule ref="PHPCompatibility"/>
<config name="testVersion" value="5.6-"/>
<config name="testVersion" value="7.0-"/>
</ruleset>
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ class MyUnitTest extends TestCase
To add PHPUnit Markup Assertions to your project, first install the library via Composer:

```sh
$ composer require --dev stevegrunwell/phpunit-markup-assertions
composer require --dev stevegrunwell/phpunit-markup-assertions
```

Please note that if you need to execute these against a PHP 5.6 codebase, you'll need to use version 1.x:

```sh
composer require --dev stevegrunwell/phpunit-markup-assertions:^1.0
```

Next, import the `SteveGrunwell\PHPUnit_Markup_Assertions\MarkupAssertionsTrait` trait into each test case that will leverage the assertions:
Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"source": "https://github.com/stevegrunwell/phpunit-markup-assertions/"
},
"require": {
"php": "^5.6 || ^7.0 || ^8.0",
"symfony/css-selector": "^3.4|^4.4|^5.4|^6.0",
"symfony/dom-crawler": "^3.4|^4.4|^5.4|^6.0"
"php": "^7.0 || ^8.0",
"symfony/css-selector": "^3.4|^4.4|^5.4|^6.0|^7.0",
"symfony/dom-crawler": "^3.4|^4.4|^5.4|^6.0|^7.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ parameters:
- tests
excludePaths:
- tests/coverage
ignoreErrors:
- '#Method Tests\\.+ has no return type specified.#'
5 changes: 4 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests</directory>
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory suffix="Test.php">./tests/Integration</directory>
</testsuite>
</testsuites>
<filter>
Expand Down
54 changes: 54 additions & 0 deletions src/Constraints/ContainsSelector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace SteveGrunwell\PHPUnit_Markup_Assertions\Constraints;

use PHPUnit\Framework\Constraint\Constraint;
use SteveGrunwell\PHPUnit_Markup_Assertions\DOM;
use SteveGrunwell\PHPUnit_Markup_Assertions\Selector;

/**
* Evaluate whether or not markup contains at least one instance of the given selector.
*/
class ContainsSelector extends Constraint
{
use ExporterTrait;

/**
* @var Selector
*/
private $selector;

/**
* @param Selector $selector The query selector.
*/
public function __construct(Selector $selector)
{
parent::__construct();

Check failure on line 26 in src/Constraints/ContainsSelector.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined static method PHPUnit\Framework\Constraint\Constraint::__construct().

$this->selector = $selector;
}

/**
* {@inheritDoc}
*
* @return string
*/
public function toString(): string
{
return 'contains selector ' . $this->exportValue($this->selector->getValue());
}

/**
* {@inheritDoc}
*
* @param mixed $html The HTML to evaluate.
*
* @return bool
*/
protected function matches($html): bool
{
$dom = new DOM($html);

return $dom->countInstancesOfSelector($this->selector) > 0;
}
}
151 changes: 151 additions & 0 deletions src/Constraints/ElementContainsString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace SteveGrunwell\PHPUnit_Markup_Assertions\Constraints;

use PHPUnit\Framework\Constraint\Constraint;
use SteveGrunwell\PHPUnit_Markup_Assertions\DOM;
use SteveGrunwell\PHPUnit_Markup_Assertions\Selector;

/**
* Evaluate whether or not the element(s) matching the given selector contain a given string.
*/
class ElementContainsString extends Constraint
{
use ExporterTrait;

/**
* A cache of matches that we have checked against.
*
* @var array<string>
*/
protected $matchingElements = [];

/**
* @var Selector
*/
protected $selector;

/**
* @var bool
*/
private $ignore_case;

/**
* @var string
*/
private $needle;

/**
* @param Selector $selector The query selector.
* @param string $needle The string to search for within the matching element(s).
* @param bool $ignore_case Optional. If true, search in a case-insensitive fashion.
* Default is false (case-sensitive searching).
*/
public function __construct(Selector $selector, $needle, $ignore_case = false)
{
parent::__construct();

Check failure on line 46 in src/Constraints/ElementContainsString.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to an undefined static method PHPUnit\Framework\Constraint\Constraint::__construct().

$this->selector = $selector;
$this->needle = $needle;
$this->ignore_case = $ignore_case;
}

/**
* {@inheritDoc}
*
* @return string
*/
public function toString(): string
{
return sprintf(
'%s string %s',
count($this->matchingElements) >= 2 ? 'contain' : 'contains',
$this->exportValue($this->needle)
);
}

/**
* Return additional failure description where needed.
*
* The function can be overridden to provide additional failure
* information like a diff
*
* @param mixed $other evaluated value or object
*/
protected function additionalFailureDescription($other): string
{
if (empty($this->matchingElements)) {
return '';
}

return sprintf(
"%s\n%s",
count($this->matchingElements) >= 2 ? 'Matching elements:' : 'Matching element:',
$this->exportMatchesArray($this->matchingElements)
);
}

/**
* Export an array of DOM matches for a selector.
*
* @param array<string> $matches
*
* @return string
*/
protected function exportMatchesArray(array $matches): string
{
$matches = array_map('trim', $matches);

return '[' . PHP_EOL . ' ' . implode(PHP_EOL . ' ', $matches) . PHP_EOL . ']';
}

/**
* {@inheritDoc}
*
* @param mixed $html The evaluated markup. Will not actually be used, instead replaced with
* {@see $this->matches}.
*
* @return string
*/
protected function failureDescription($html): string
{
if (empty($this->matchingElements)) {
return "any elements match selector '{$this->selector->getValue()}'";
}

$label = count($this->matchingElements) >= 2
? 'any elements matching selector %s %s'
: 'element matching selector %s %s';

return sprintf(
$label,
$this->exportValue($this->selector->getValue()),
$this->toString()
);
}

/**
* {@inheritDoc}
*
* @param mixed $html The HTML to match against.
*
* @return bool
*/
protected function matches($html): bool
{
$dom = new DOM($html);
$fn = $this->ignore_case ? 'stripos' : 'strpos';

// Iterate through each matching element and look for the text.
foreach ($dom->getInnerHtml($this->selector) as $html) {
if ($fn($html, $this->needle) !== false) {
return true;
}
}

// Query again to get the outer elements for error reporting.
$this->matchingElements = $dom->getOuterHtml($this->selector);

return false;
}
}
70 changes: 70 additions & 0 deletions src/Constraints/ElementMatchesRegExp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace SteveGrunwell\PHPUnit_Markup_Assertions\Constraints;

use SteveGrunwell\PHPUnit_Markup_Assertions\DOM;
use SteveGrunwell\PHPUnit_Markup_Assertions\Selector;
use Symfony\Component\DomCrawler\Crawler;

/**
* Evaluate whether or not the contents of elements matching the given $selector match the given
* PCRE regular expression.
*/
class ElementMatchesRegExp extends ElementContainsString
{
/**
* @var string
*/
private $pattern;

/**
* @param Selector $selector The query selector.
* @param string $pattern The regular expression pattern to test against the matching element(s).
*/
public function __construct(Selector $selector, string $pattern)
{
parent::__construct($selector, '');

$this->selector = $selector;
$this->pattern = $pattern;
}

/**
* {@inheritDoc}
*
* @return string
*/
public function toString(): string
{
return sprintf(
'%s regular expression %s',
count($this->matchingElements) >= 2 ? 'match' : 'matches',
$this->exportValue($this->pattern)
);
}

/**
* Evaluates the constraint for parameter $other. Returns true if the
* constraint is met, false otherwise.
*
* @param mixed $html value or object to evaluate
*
* @return bool
*/
protected function matches($html): bool
{
$dom = new DOM($html);

// Iterate through each matching element and look for the pattern.
foreach ($dom->getInnerHtml($this->selector) as $html) {
if (preg_match($this->pattern, $html)) {
return true;
}
}

// Query again to get the outer elements for error reporting.
$this->matchingElements = $dom->getOuterHtml($this->selector);

return false;
}
}
38 changes: 38 additions & 0 deletions src/Constraints/ExporterTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace SteveGrunwell\PHPUnit_Markup_Assertions\Constraints;

use SebastianBergmann\Exporter\Exporter;

/**
* PHPUnit's exporter() method wasn't introduced until PHPUnit 8, which causes errors when we try
* to call $this->exporter()->export() in assertions.
*
* Instead, this trait exposes an exportValue() method that verifies that the relevant export
* method is present.
*/
trait ExporterTrait
{
/**
* Exports a value as a string.
*
* @param mixed $value The value to be exported.
*
* @return string A string representation.
*/
protected function exportValue($value): string
{
// PHPUnit 8.x and newer only instantiate the exporter when needed.
if (method_exists($this, 'exporter') && $this->exporter() instanceof Exporter) {
return $this->exporter()->export($value);
}

// PHPUnit 7.x creates the exporter in the constructor.
if (isset($this->exporter) && $this->exporter instanceof Exporter) {
return $this->exporter->export($value);
}

// For everything else, just use var_export() and hope for the best.
return var_export($value, true);
}
}
Loading
Loading