Add PHPUnit test suite with Brain\Monkey (v0.1.6)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m2s
Create Release Package / PHPUnit Tests (push) Successful in 46s
Create Release Package / Build Release (push) Successful in 50s

Add test infrastructure for isolated unit testing without WordPress/WooCommerce:
- 27 tests (54 assertions) covering TemplateOverride and WooCommerceExtension
- Brain\Monkey for WordPress function mocking, class stubs for TwigService and WC_Product
- PHPUnit test job added to Gitea CI pipeline between lint and build-release
- Test artifacts excluded from release packages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 03:48:19 +01:00
parent 784b400c46
commit 4031a1c8aa
14 changed files with 2678 additions and 10 deletions

View File

@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace WcBootstrap\Tests\Unit\Twig;
use PHPUnit\Framework\TestCase;
use Brain\Monkey;
use Brain\Monkey\Functions;
use Twig\TwigFunction;
use Twig\TwigFilter;
use WcBootstrap\Twig\WooCommerceExtension;
class WooCommerceExtensionTest extends TestCase
{
private WooCommerceExtension $extension;
protected function setUp(): void
{
parent::setUp();
Monkey\setUp();
$this->extension = new WooCommerceExtension();
}
protected function tearDown(): void
{
Monkey\tearDown();
parent::tearDown();
}
// ── getFunctions / getFilters ───────────────────────────────
public function testGetFunctionsReturnsNonEmptyArray(): void
{
$functions = $this->extension->getFunctions();
$this->assertNotEmpty($functions);
$this->assertContainsOnlyInstancesOf(TwigFunction::class, $functions);
}
public function testGetFiltersReturnsNonEmptyArray(): void
{
$filters = $this->extension->getFilters();
$this->assertNotEmpty($filters);
$this->assertContainsOnlyInstancesOf(TwigFilter::class, $filters);
}
public function testGetFunctionsContainsExpectedNames(): void
{
$names = array_map(
fn(TwigFunction $f) => $f->getName(),
$this->extension->getFunctions()
);
$expected = [
'do_action', 'apply_filters', 'wp_nonce_field', 'fn',
'wc_get_cart_url', 'wc_get_checkout_url', 'wc_get_template',
'wc_print_notices', 'woocommerce_form_field',
];
foreach ($expected as $name) {
$this->assertContains($name, $names, "Missing Twig function: {$name}");
}
}
public function testGetFiltersContainsExpectedNames(): void
{
$names = array_map(
fn(TwigFilter $f) => $f->getName(),
$this->extension->getFilters()
);
$expected = [
'esc_html', 'esc_attr', 'esc_url',
'sanitize_title', 'wpautop', 'wp_kses_post',
'wptexturize', 'do_shortcode',
];
foreach ($expected as $name) {
$this->assertContains($name, $names, "Missing Twig filter: {$name}");
}
}
// ── callFunction() whitelist ────────────────────────────────
public function testCallFunctionWithAllowedFunctionSucceeds(): void
{
// Define a stub for an allowed function.
Functions\when('_n')->alias(fn($single, $plural, $count) => $count === 1 ? $single : $plural);
$result = $this->extension->callFunction('_n', 'item', 'items', 1);
$this->assertSame('item', $result);
}
public function testCallFunctionWithDisallowedFunctionThrows(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('is not allowed');
$this->extension->callFunction('exec', 'whoami');
}
public function testCallFunctionWithNonExistentAllowedFunctionThrows(): void
{
// 'WC' is allowed but won't exist in the test environment.
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('does not exist');
$this->extension->callFunction('WC');
}
// ── doAction() ──────────────────────────────────────────────
public function testDoActionCapturesOutput(): void
{
Functions\when('do_action')->alias(function (string $tag): void {
echo "<div>{$tag}</div>";
});
$result = $this->extension->doAction('woocommerce_before_cart');
$this->assertSame('<div>woocommerce_before_cart</div>', $result);
}
public function testDoActionReturnsEmptyWhenNoOutput(): void
{
Functions\when('do_action')->justReturn(null);
$result = $this->extension->doAction('some_silent_hook');
$this->assertSame('', $result);
}
// ── applyFilters() ──────────────────────────────────────────
public function testApplyFiltersDelegatesToWordPress(): void
{
Functions\when('apply_filters')->alias(function (string $tag, ...$args) {
return strtoupper($args[0]);
});
$result = $this->extension->applyFilters('the_title', 'hello');
$this->assertSame('HELLO', $result);
}
// ── callUserFunc() ──────────────────────────────────────────
public function testCallUserFuncCapturesCallbackOutput(): void
{
$result = $this->extension->callUserFunc(function (): void {
echo 'tab-content';
});
$this->assertSame('tab-content', $result);
}
public function testCallUserFuncPassesArguments(): void
{
$result = $this->extension->callUserFunc(function (string $key): void {
echo "key={$key}";
}, 'description');
$this->assertSame('key=description', $result);
}
// ── setupProductData() ──────────────────────────────────────
public function testSetupProductDataSetsGlobalProduct(): void
{
$product = new \WC_Product(99);
Functions\when('get_post')->justReturn((object) ['ID' => 99]);
Functions\when('setup_postdata')->justReturn(true);
$result = $this->extension->setupProductData($product);
$this->assertSame('', $result);
$this->assertSame($product, $GLOBALS['product']);
unset($GLOBALS['product'], $GLOBALS['post']);
}
public function testSetupProductDataSkipsPostdataWhenPostNotFound(): void
{
$product = new \WC_Product(1);
Functions\when('get_post')->justReturn(null);
$result = $this->extension->setupProductData($product);
$this->assertSame('', $result);
$this->assertSame($product, $GLOBALS['product']);
unset($GLOBALS['product']);
}
// ── Output-capture wrappers ─────────────────────────────────
public function testWcPrintNoticesCapturesOutput(): void
{
Functions\when('wc_print_notices')->alias(function (): void {
echo '<div class="woocommerce-message">Notice</div>';
});
$result = $this->extension->wcPrintNotices();
$this->assertStringContainsString('woocommerce-message', $result);
}
public function testWcDisplayItemMetaReturnsString(): void
{
$item = new \stdClass();
Functions\when('wc_display_item_meta')->alias(function ($item, array $args): string {
return $args['echo'] === false ? '<span>meta</span>' : '';
});
$result = $this->extension->wcDisplayItemMeta($item, []);
$this->assertSame('<span>meta</span>', $result);
}
public function testWcQueryStringFormFieldsReturnsHtml(): void
{
Functions\when('wc_query_string_form_fields')->alias(function ($values, array $exclude, string $key, bool $return): string {
return $return ? '<input type="hidden">' : '';
});
$result = $this->extension->wcQueryStringFormFields(null, [], '');
$this->assertSame('<input type="hidden">', $result);
}
public function testWoocommerceFormFieldReturnsHtml(): void
{
Functions\when('woocommerce_form_field')->alias(function (string $key, array $args, $value): string {
return $args['return'] === true ? "<p class=\"form-row\">{$key}</p>" : '';
});
$result = $this->extension->woocommerceFormField('billing_first_name', [], null);
$this->assertStringContainsString('billing_first_name', $result);
}
public function testWcGetTemplateCapturesOutput(): void
{
Functions\when('wc_get_template')->alias(function (string $name): void {
echo "<div>{$name}</div>";
});
$result = $this->extension->wcGetTemplate('order/order-details.php', []);
$this->assertSame('<div>order/order-details.php</div>', $result);
}
}