Skip to content

Commit

Permalink
Element assertions and HTML String (#528)
Browse files Browse the repository at this point in the history
* Add macro support to factories

* Adding HTML String class

* Fixing helper

* Linting fixes

* CHANGELOG

* Add assertContains/assertNotContains methods

* Test for ID

* Adding assertion messages

* CHANGELOG date

* Phpstan fixes
  • Loading branch information
srtfisher authored Apr 15, 2024
1 parent 6e7f045 commit 5a0f8f3
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 49 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v1.0.2 - 2024-04-10
## v1.0.2 - 2024-04-15

### Added

- Added `html_string()` helper to make assertions against a HTML string easier in testing.
- Added new assertion methods to test against elements.

### Fixed

Expand Down
12 changes: 9 additions & 3 deletions src/mantle/database/factory/class-factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
* @method \Mantle\Database\Factory\Fluent_Factory<TModel, TObject, TReturnValue> count(int $count)
*/
abstract class Factory {
use Concerns\Resolves_Factories,
Conditionable,
Macroable;
use Concerns\Resolves_Factories;
use Conditionable;
use Macroable {
__call as macro_call;
}

/**
* Flag to return the factory as a model.
Expand Down Expand Up @@ -276,6 +278,10 @@ protected function create_fluent_factory(): Fluent_Factory {
* @param array $args The arguments.
*/
public function __call( string $method, array $args ): mixed {
if ( static::has_macro( $method ) ) {
return $this->macro_call( $method, $args );
}

return $this->create_fluent_factory()->$method( ...$args );
}
}
1 change: 1 addition & 0 deletions src/mantle/testing/autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use function Mantle\Support\Helpers\tap;

require_once __DIR__ . '/preload.php';
require_once __DIR__ . '/helpers/helpers-element-assertions.php';
require_once __DIR__ . '/helpers/helpers-http-response.php';
require_once __DIR__ . '/mail/helpers.php';

Expand Down
58 changes: 58 additions & 0 deletions src/mantle/testing/class-assertable-html-string.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/**
* HTML_String class file
*
* phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
*
* @package Mantle
*/

namespace Mantle\Testing;

use Mantle\Testing\Concerns\Element_Assertions;
use PHPUnit\Framework\Assert;

/**
* HTML String
*
* Perform assertions against a HTML string.
*/
class Assertable_HTML_String {
use Element_Assertions;

/**
* Constructor.
*
* @param string $content The HTML content to test.
*/
public function __construct( protected string $content ) {}

/**
* Retrieve the content.
*/
protected function get_content(): string {
return $this->content;
}

/**
* Assert that the content contains the expected string.
*
* @param string $needle The $needle to assert against.
*/
public function assertContains( string $needle ): static {
Assert::assertStringContainsString( $needle, $this->content, 'The content does not contain the expected string.' );

return $this;
}

/**
* Assert that the content does not contain the expected string.
*
* @param string $needle The $needle to assert against.
*/
public function assertNotContains( string $needle ): static {
Assert::assertStringNotContainsString( $needle, $this->content, 'The content contains the unexpected string.' );

return $this;
}
}
4 changes: 0 additions & 4 deletions src/mantle/testing/class-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,10 +464,6 @@ public static function command( $command, &$exit_code = null ) {
* Ensure that Composer is loaded for the current environment.
*/
public static function ensure_composer_loaded(): void {
if ( class_exists( \Composer\Autoload\ClassLoader::class ) ) {
return;
}

$paths = [
preg_replace( '#/vendor/.*$#', '/vendor/autoload.php', __DIR__ ),
__DIR__ . '/../../../vendor/autoload.php',
Expand Down
123 changes: 112 additions & 11 deletions src/mantle/testing/concerns/trait-element-assertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
/**
* Element_Assertions trait file
*
* phpcs:disable Squiz.Commenting.FunctionComment.ParamNameNoMatch, Squiz.Commenting.FunctionComment.MissingParamTag
*
* @package Mantle
*/

namespace Mantle\Testing\Concerns;

use PHPUnit\Framework\Assert as PHPUnit;
use DOMDocument;
use DOMNode;
use DOMXPath;
use Symfony\Component\CssSelector\CssSelectorConverter;

Expand Down Expand Up @@ -52,11 +55,15 @@ protected function convert_query_selector( string $selector ): string {
* Assert that an element exists in the response.
*
* @param string $expression The XPath expression to execute.
* @param string $message Optional message to display on failure.
*/
public function assertElementExists( string $expression ): static {
public function assertElementExists( string $expression, string $message = null ): static {
$nodes = ( new DOMXPath( $this->get_dom_document() ) )->query( $expression );

PHPUnit::assertTrue( ! $nodes ? false : $nodes->length > 0 );
PHPUnit::assertTrue(
! $nodes ? false : $nodes->length > 0,
$message ?? 'Element not found for expression: ' . $expression,
);

return $this;
}
Expand All @@ -71,7 +78,7 @@ public function assertElementExistsById( string $id ): static {
$id = substr( $id, 1 );
}

return $this->assertElementExists( sprintf( '//*[@id="%s"]', $id ) );
return $this->assertElementExists( sprintf( '//*[@id="%s"]', $id ), "Element not found for ID: $id" );
}

/**
Expand All @@ -84,18 +91,25 @@ public function assertElementExistsByClassName( string $classname ): static {
$classname = substr( $classname, 1 );
}

return $this->assertElementExists( sprintf( '//*[contains(concat(" ", normalize-space(@class), " "), " %s ")]', $classname ) );
return $this->assertElementExists(
sprintf( '//*[contains(concat(" ", normalize-space(@class), " "), " %s ")]', $classname ),
"Element not found for class: $classname"
);
}

/**
* Assert that an element is missing in the response.
*
* @param string $expression The XPath expression to execute.
* @param string $message The message to display if the assertion fails.
*/
public function assertElementMissing( string $expression ): static {
public function assertElementMissing( string $expression, string $message = null ): static {
$nodes = ( new DOMXPath( $this->get_dom_document() ) )->query( $expression );

PHPUnit::assertTrue( false === $nodes || 0 === $nodes->length );
PHPUnit::assertTrue(
false === $nodes || 0 === $nodes->length,
$message ?? "Element found for expression: $expression"
);

return $this;
}
Expand All @@ -110,7 +124,7 @@ public function assertElementMissingById( string $id ): static {
$id = substr( $id, 1 );
}

return $this->assertElementMissing( sprintf( '//*[@id="%s"]', $id ) );
return $this->assertElementMissing( sprintf( '//*[@id="%s"]', $id ), "Element found for ID: $id" );
}

/**
Expand All @@ -132,7 +146,7 @@ public function assertElementMissingByClassName( string $classname ): static {
* @param string $type The type of element to check.
*/
public function assertElementExistsByTagName( string $type ): static {
return $this->assertElementExists( sprintf( '//*[local-name()="%s"]', $type ) );
return $this->assertElementExists( sprintf( '//*[local-name()="%s"]', $type ), "Element not found for tag: $type" );
}

/**
Expand All @@ -141,7 +155,7 @@ public function assertElementExistsByTagName( string $type ): static {
* @param string $type The type of element to check.
*/
public function assertElementMissingByTagName( string $type ): static {
return $this->assertElementMissing( sprintf( '//*[local-name()="%s"]', $type ) );
return $this->assertElementMissing( sprintf( '//*[local-name()="%s"]', $type ), "Element found for tag: $type" );
}

/**
Expand All @@ -150,7 +164,7 @@ public function assertElementMissingByTagName( string $type ): static {
* @param string $selector The selector to use.
*/
public function assertElementExistsByQuerySelector( string $selector ): static {
return $this->assertElementExists( $this->convert_query_selector( $selector ) );
return $this->assertElementExists( $this->convert_query_selector( $selector ), "Element not found for selector: $selector" );
}

/**
Expand All @@ -168,7 +182,7 @@ public function assertQuerySelectorExists( string $selector ): static {
* @param string $selector The selector to use.
*/
public function assertElementMissingByQuerySelector( string $selector ): static {
return $this->assertElementMissing( $this->convert_query_selector( $selector ) );
return $this->assertElementMissing( $this->convert_query_selector( $selector ), "Element found for selector: $selector" );
}

/**
Expand All @@ -179,4 +193,91 @@ public function assertElementMissingByQuerySelector( string $selector ): static
public function assertQuerySelectorMissing( string $selector ): static {
return $this->assertElementMissingByQuerySelector( $selector );
}

/**
* Assert against the expected number of elements for an expression.
*
* @param string $expression The XPath expression to execute.
* @param int $expected The expected number of elements.
*/
public function assertElementCount( string $expression, int $expected ): static {
$nodes = ( new DOMXPath( $this->get_dom_document() ) )->query( $expression );

PHPUnit::assertEquals( $expected, $nodes->length, 'Unexpected number of elements found.' );

return $this;
}

/**
* Assert against the expected number of elements for a query selector.
*
* @param string $selector The selector to use.
* @param int $expected The expected number of elements.
*/
public function assertQuerySelectorCount( string $selector, int $expected ): static {
return $this->assertElementCount( $this->convert_query_selector( $selector ), $expected );
}

/**
* Assert an element exists by test ID.
*
* @param string $test_id The test ID to check.
*/
public function assertElementExistsByTestId( string $test_id ): static {
return $this->assertQuerySelectorExists( "[data-testid=\"$test_id\"]" );
}

/**
* Assert an element is missing by test ID.
*
* @param string $test_id The test ID to check.
*/
public function assertElementMissingByTestId( string $test_id ): static {
return $this->assertQuerySelectorMissing( "[data-testid=\"$test_id\"]" );
}

/**
* Assert that an element passes a custom assertion.
*
* The assertion will be called for each node found by the expression.
*
* @param string $expression The XPath expression to execute.
* @param callable(DOMNode $node): bool $assertion The assertion to run.
* @param bool $pass_any Pass if any of the nodes pass the assertion. Otherwise, all must pass.
*/
public function assertElement( string $expression, callable $assertion, bool $pass_any = false ): static {
$nodes = ( new DOMXPath( $this->get_dom_document() ) )->query( $expression );

if ( ! $nodes ) {
PHPUnit::fail( 'No nodes found for expression: ' . $expression );
}

foreach ( $nodes as $node ) {
if ( $assertion( $node ) ) {
// If we're passing on any, we can return early.
if ( $pass_any ) {
return $this;
}
} elseif ( ! $pass_any ) {
PHPUnit::fail( 'Assertion failed for node: ' . $node->nodeName ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
}

PHPUnit::assertTrue( true, 'All nodes passed assertion.' );

return $this;
}

/**
* Assert that an element passes a custom assertion by query selector.
*
* The assertion will be called for each node found by the selector.
*
* @param string $selector The selector to use.
* @param callable(DOMNode $node): bool $assertion The assertion to run.
* @param bool $pass_any Pass if any of the nodes pass the assertion. Otherwise, all must pass.
*/
public function assertQuerySelector( string $selector, callable $assertion, bool $pass_any = false ): static {
return $this->assertElement( $this->convert_query_selector( $selector ), $assertion, $pass_any );
}
}
17 changes: 17 additions & 0 deletions src/mantle/testing/helpers/helpers-element-assertions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/**
* Helper for assertions against elements.
*
* @package Mantle
*/

namespace Mantle\Testing;

/**
* Create a new HTML_String instance.
*
* @param string $html The HTML string to test.
*/
function html_string( string $html ): Assertable_HTML_String {
return new Assertable_HTML_String( $html );
}
12 changes: 12 additions & 0 deletions tests/Database/Factory/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Mantle\Tests\Database\Factory;

use Mantle\Database\Factory;
use Mantle\Database\Factory\Post_Factory;
use Mantle\Database\Model;
use Mantle\Testing\Framework_Test_Case;

Expand Down Expand Up @@ -118,6 +119,17 @@ public function test_create_multiple_fluently_with_scopes() {
);
}

public function test_factory_macro() {
Post_Factory::macro(
'with_custom_meta',
fn () => $this->with_meta( [ 'custom_meta_key' => 'custom_meta_value' ] ),
);

$post_id = Testable_Post::factory()->with_custom_meta()->create();

$this->assertEquals( 'custom_meta_value', get_post_meta( $post_id, 'custom_meta_key', true ) );
}

public function test_create_custom_post_type_model() {
register_post_type(
'custom_post_type',
Expand Down
Loading

0 comments on commit 5a0f8f3

Please sign in to comment.