You've already forked wc-composable-product
Add PHPUnit test suite, PSR-4 refactor, lint+test CI jobs (v1.3.1)
- 57 unit tests covering ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings using Brain Monkey + Mockery - WooCommerce class stubs for testing without WP installation - PHP lint and test jobs in release workflow (test gate blocks release) - PSR-4 namespace change: WC_Composable_Product -> Magdev\WcComposableProduct - PascalCase filenames for all classes under includes/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,52 @@ on:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: PHP Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml, zip
|
||||
tools: composer:v2
|
||||
|
||||
- name: PHP Syntax Check
|
||||
run: |
|
||||
find includes -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||
find tests -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: mbstring, xml
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: |
|
||||
composer config platform.php 8.3.0
|
||||
composer install --optimize-autoloader --no-interaction
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
run: vendor/bin/phpunit --testdox
|
||||
|
||||
build-release:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -18,6 +18,10 @@ logs/
|
||||
Thumbs.db
|
||||
.directory
|
||||
|
||||
# PHPUnit local overrides
|
||||
phpunit.xml
|
||||
|
||||
# Binary files
|
||||
languages/*.mo
|
||||
|
||||
.phpunit.result.cache
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,23 @@ 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).
|
||||
|
||||
## [1.3.1] - 2026-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- **PHPUnit test suite**: 57 unit tests covering all 6 core classes (ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings)
|
||||
- **Brain Monkey + Mockery** for WordPress/WooCommerce function mocking without a full WP installation
|
||||
- **WooCommerce class stubs** in `tests/stubs/` for classes extended by the plugin (WC_Product, WC_Settings_Page, etc.)
|
||||
- **PHP lint job** in release workflow — syntax-checks all PHP files before testing
|
||||
- **Test job** in release workflow — tests must pass before release package is built (`needs: test`)
|
||||
- Testing section in README
|
||||
|
||||
### Changed
|
||||
|
||||
- **PSR-4 refactored**: Renamed files to PascalCase (Product_Type → ProductType, etc.) and changed namespace from `WC_Composable_Product` to `Magdev\WcComposableProduct`
|
||||
- Updated all cross-references in PHP files, main plugin file, composer.json, CSS/JS doc comments, and translation file source comments
|
||||
- Release workflow now has three stages: lint → test → build-release
|
||||
|
||||
## [1.3.0] - 2026-03-01
|
||||
|
||||
### Added
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -18,6 +18,7 @@ This project is 100% AI-generated ("vibe-coded") using Claude.AI.
|
||||
- **Styling:** Custom CSS
|
||||
- **Dependencies:** Composer
|
||||
- **i18n:** WordPress i18n (.pot/.po/.mo), text domain: `wc-composable-product`
|
||||
- **Testing:** PHPUnit 9.6 + Brain Monkey 2.7 + Mockery 1.6
|
||||
- **CI/CD:** Gitea Actions (`.gitea/workflows/release.yml`)
|
||||
|
||||
## Project Structure
|
||||
@@ -45,10 +46,16 @@ wc-composable-product/
|
||||
│ └── StockManager.php # Stock management & inventory tracking
|
||||
├── languages/ # Translation files (.pot, .po, .mo)
|
||||
├── releases/ # Release packages (gitignored)
|
||||
├── tests/
|
||||
│ ├── bootstrap.php # Test environment setup (constants, stubs)
|
||||
│ ├── TestCase.php # Base test case with Brain Monkey
|
||||
│ ├── stubs/ # Minimal WooCommerce class stubs
|
||||
│ └── Unit/ # PHPUnit unit tests
|
||||
├── templates/
|
||||
│ └── product-selector.twig # Frontend selection interface
|
||||
├── vendor/ # Composer dependencies (gitignored, included in releases)
|
||||
├── composer.json
|
||||
├── phpunit.xml.dist # PHPUnit configuration
|
||||
└── wc-composable-product.php # Main plugin file
|
||||
```
|
||||
|
||||
@@ -133,11 +140,17 @@ Compile .po to .mo: `for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"
|
||||
|
||||
WordPress requires compiled .mo files — .po files alone are insufficient.
|
||||
|
||||
## Testing
|
||||
|
||||
Run unit tests: `vendor/bin/phpunit --testdox`
|
||||
|
||||
Tests use **Brain Monkey** to mock WordPress/WooCommerce functions without a full WP installation. WooCommerce classes (`WC_Product`, `WC_Settings_Page`, etc.) are provided as minimal stubs in `tests/stubs/` so PHP can parse `extends` declarations. The release workflow runs tests before building — a failing test blocks the release.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
### Automated (Gitea CI/CD)
|
||||
|
||||
Push an annotated tag (`v*`) to trigger the workflow. It installs PHP 8.3, production Composer deps, compiles translations, verifies version matches tag, creates ZIP with checksums, and publishes a Gitea release.
|
||||
Push an annotated tag (`v*`) to trigger the workflow. It first runs the PHPUnit test suite, then installs PHP 8.3, production Composer deps, compiles translations, verifies version matches tag, creates ZIP with checksums, and publishes a Gitea release. Tests must pass before the release package is built.
|
||||
|
||||
### Manual
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -12,7 +12,8 @@ Create composable products where customers can select a limited number of items
|
||||
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
||||
- **Multi-language Support**: Fully translated in 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
||||
- **CI/CD**: Automated release workflow for Gitea
|
||||
- **Tested**: 57 unit tests with PHPUnit, Brain Monkey, and Mockery
|
||||
- **CI/CD**: Automated release workflow with test gate for Gitea
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -118,6 +119,16 @@ This project was created with AI assistance (Claude.AI) and follows WordPress an
|
||||
composer install
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
The plugin includes a PHPUnit test suite with Brain Monkey for WordPress function mocking:
|
||||
|
||||
```bash
|
||||
vendor/bin/phpunit --testdox
|
||||
```
|
||||
|
||||
Tests run without a WordPress installation. WooCommerce classes are provided as minimal stubs in `tests/stubs/`.
|
||||
|
||||
### Translation
|
||||
|
||||
Generate POT file:
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
"Magdev\\WcComposableProduct\\": "includes/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"brain/monkey": "^2.7",
|
||||
"mockery/mockery": "^1.6",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Magdev\\WcComposableProduct\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
|
||||
2065
composer.lock
generated
2065
composer.lock
generated
File diff suppressed because it is too large
Load Diff
24
phpunit.xml.dist
Normal file
24
phpunit.xml.dist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
verbose="true"
|
||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="false"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">includes</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
75
tests/TestCase.php
Normal file
75
tests/TestCase.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Base Test Case
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
||||
use Brain\Monkey;
|
||||
|
||||
/**
|
||||
* Base test case with Brain Monkey and Mockery integration.
|
||||
*
|
||||
* All test classes should extend this instead of PHPUnit\Framework\TestCase.
|
||||
*/
|
||||
abstract class TestCase extends PHPUnitTestCase
|
||||
{
|
||||
use MockeryPHPUnitIntegration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Monkey\setUp();
|
||||
|
||||
// Stub common WordPress translation functions (__(), _e(), esc_html__(), etc.)
|
||||
Monkey\Functions\stubTranslationFunctions();
|
||||
|
||||
// Stub common WordPress escaping functions (esc_html(), esc_attr(), etc.)
|
||||
Monkey\Functions\stubEscapeFunctions();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Monkey\tearDown();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Mockery mock of WC_Product with sensible defaults.
|
||||
*
|
||||
* @param array $overrides Method return value overrides
|
||||
* @return \Mockery\MockInterface
|
||||
*/
|
||||
protected function createProductMock(array $overrides = []): \Mockery\MockInterface
|
||||
{
|
||||
$defaults = [
|
||||
'get_id' => 100,
|
||||
'get_name' => 'Test Product',
|
||||
'get_type' => 'simple',
|
||||
'get_price' => '10.00',
|
||||
'get_regular_price' => '10.00',
|
||||
'get_price_html' => '<span>$10.00</span>',
|
||||
'get_permalink' => 'https://example.com/product/test',
|
||||
'get_image_id' => 1,
|
||||
'get_stock_quantity' => null,
|
||||
'get_stock_status' => 'instock',
|
||||
'is_purchasable' => true,
|
||||
'is_in_stock' => true,
|
||||
'managing_stock' => false,
|
||||
'backorders_allowed' => false,
|
||||
];
|
||||
|
||||
$config = array_merge($defaults, $overrides);
|
||||
$mock = \Mockery::mock('WC_Product');
|
||||
|
||||
foreach ($config as $method => $return) {
|
||||
$mock->shouldReceive($method)->andReturn($return)->byDefault();
|
||||
}
|
||||
|
||||
return $mock;
|
||||
}
|
||||
}
|
||||
108
tests/Unit/Admin/ProductDataTest.php
Normal file
108
tests/Unit/Admin/ProductDataTest.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin ProductData Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\Admin\ProductData;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class ProductDataTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_POST = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testConstructor_RegistersExpectedHooks(): void
|
||||
{
|
||||
$productData = new ProductData();
|
||||
|
||||
self::assertNotFalse(has_filter('woocommerce_product_data_tabs', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_tab()'));
|
||||
self::assertNotFalse(has_action('woocommerce_product_data_panels', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_panel()'));
|
||||
self::assertNotFalse(has_action('woocommerce_process_product_meta_composable', 'Magdev\WcComposableProduct\Admin\ProductData->save_product_data()'));
|
||||
self::assertNotFalse(has_action('woocommerce_product_options_general_product_data', 'Magdev\WcComposableProduct\Admin\ProductData->add_general_fields()'));
|
||||
}
|
||||
|
||||
public function testAddProductDataTab_AddsComposableTab(): void
|
||||
{
|
||||
$productData = new ProductData();
|
||||
$tabs = $productData->add_product_data_tab([]);
|
||||
|
||||
$this->assertArrayHasKey('composable', $tabs);
|
||||
$this->assertSame('composable_product_data', $tabs['composable']['target']);
|
||||
$this->assertContains('show_if_composable', $tabs['composable']['class']);
|
||||
$this->assertSame(21, $tabs['composable']['priority']);
|
||||
}
|
||||
|
||||
public function testSaveProductData_SavesAllFields(): void
|
||||
{
|
||||
$_POST = [
|
||||
'_composable_selection_limit' => '5',
|
||||
'_composable_pricing_mode' => 'fixed',
|
||||
'_composable_include_unpublished' => 'yes',
|
||||
'_composable_criteria_type' => 'tag',
|
||||
'_composable_categories' => ['1', '2'],
|
||||
'_composable_tags' => ['3', '4'],
|
||||
'_composable_skus' => 'SKU-1, SKU-2',
|
||||
];
|
||||
|
||||
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
|
||||
Functions\expect('update_post_meta')->times(7);
|
||||
|
||||
$productData = new ProductData();
|
||||
$productData->save_product_data(42);
|
||||
}
|
||||
|
||||
public function testSaveProductData_DefaultsWhenPostEmpty(): void
|
||||
{
|
||||
// No POST data at all
|
||||
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
|
||||
return $val;
|
||||
});
|
||||
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_selection_limit', \Mockery::any())->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_pricing_mode', '')->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_include_unpublished', '')->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_criteria_type', 'category')->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_categories', [])->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_tags', [])->once();
|
||||
Functions\expect('update_post_meta')
|
||||
->with(42, '_composable_skus', '')->once();
|
||||
|
||||
$productData = new ProductData();
|
||||
$productData->save_product_data(42);
|
||||
}
|
||||
}
|
||||
84
tests/Unit/Admin/SettingsTest.php
Normal file
84
tests/Unit/Admin/SettingsTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Settings Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\Admin\Settings;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class SettingsTest extends TestCase
|
||||
{
|
||||
public function testConstructor_SetsIdAndLabel(): void
|
||||
{
|
||||
$settings = new Settings();
|
||||
|
||||
$this->assertSame('composable_products', $settings->get_id());
|
||||
}
|
||||
|
||||
public function testGetSettings_ReturnsExpectedFieldIds(): void
|
||||
{
|
||||
Functions\expect('apply_filters')
|
||||
->once()
|
||||
->with('wc_composable_settings', \Mockery::type('array'))
|
||||
->andReturnUsing(function ($hook, $settings) {
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$settings = new Settings();
|
||||
$fields = $settings->get_settings();
|
||||
|
||||
// Extract all field IDs
|
||||
$ids = array_column($fields, 'id');
|
||||
|
||||
$this->assertContains('wc_composable_settings', $ids);
|
||||
$this->assertContains('wc_composable_default_limit', $ids);
|
||||
$this->assertContains('wc_composable_default_pricing', $ids);
|
||||
$this->assertContains('wc_composable_include_unpublished', $ids);
|
||||
$this->assertContains('wc_composable_show_images', $ids);
|
||||
$this->assertContains('wc_composable_show_prices', $ids);
|
||||
$this->assertContains('wc_composable_show_total', $ids);
|
||||
}
|
||||
|
||||
public function testGetSettings_HasCorrectFieldTypes(): void
|
||||
{
|
||||
Functions\expect('apply_filters')
|
||||
->once()
|
||||
->andReturnUsing(function ($hook, $settings) {
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$settings = new Settings();
|
||||
$fields = $settings->get_settings();
|
||||
|
||||
// Index fields by ID for easy lookup
|
||||
$indexed = [];
|
||||
foreach ($fields as $field) {
|
||||
if (isset($field['id'])) {
|
||||
$indexed[$field['id']] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame('number', $indexed['wc_composable_default_limit']['type']);
|
||||
$this->assertSame('select', $indexed['wc_composable_default_pricing']['type']);
|
||||
$this->assertSame('checkbox', $indexed['wc_composable_include_unpublished']['type']);
|
||||
$this->assertSame('checkbox', $indexed['wc_composable_show_images']['type']);
|
||||
}
|
||||
|
||||
public function testGetSettings_AppliesFilter(): void
|
||||
{
|
||||
Functions\expect('apply_filters')
|
||||
->once()
|
||||
->with('wc_composable_settings', \Mockery::type('array'))
|
||||
->andReturnUsing(function ($hook, $settings) {
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$settings = new Settings();
|
||||
$settings->get_settings();
|
||||
}
|
||||
}
|
||||
184
tests/Unit/CartHandlerTest.php
Normal file
184
tests/Unit/CartHandlerTest.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* CartHandler Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\CartHandler;
|
||||
use Brain\Monkey\Functions;
|
||||
use Brain\Monkey\Actions;
|
||||
use Brain\Monkey\Filters;
|
||||
|
||||
class CartHandlerTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Clean up POST superglobal between tests
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_POST = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testConstructor_RegistersExpectedHooks(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
|
||||
self::assertNotFalse(has_filter('woocommerce_add_to_cart_validation', 'Magdev\WcComposableProduct\CartHandler->validate_add_to_cart()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_add_cart_item_data', 'Magdev\WcComposableProduct\CartHandler->add_cart_item_data()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_get_cart_item_from_session', 'Magdev\WcComposableProduct\CartHandler->get_cart_item_from_session()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_get_item_data', 'Magdev\WcComposableProduct\CartHandler->display_cart_item_data()'));
|
||||
self::assertNotFalse(has_action('woocommerce_before_calculate_totals', 'Magdev\WcComposableProduct\CartHandler->calculate_cart_item_price()'));
|
||||
self::assertNotFalse(has_action('woocommerce_single_product_summary', 'Magdev\WcComposableProduct\CartHandler->render_product_selector()'));
|
||||
self::assertNotFalse(has_filter('woocommerce_is_purchasable', 'Magdev\WcComposableProduct\CartHandler->hide_default_add_to_cart()'));
|
||||
}
|
||||
|
||||
public function testHideDefaultAddToCart_ReturnsFalseForComposable(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
|
||||
$result = $handler->hide_default_add_to_cart(true, $product);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testHideDefaultAddToCart_PassesThroughForSimple(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||
|
||||
$result = $handler->hide_default_add_to_cart(true, $product);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateAddToCart_PassesThroughForNonComposable(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateAddToCart_ReturnsFalseWhenNoProductsSelected(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
Functions\expect('wc_add_notice')->once();
|
||||
|
||||
$_POST['composable_products'] = [];
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testValidateAddToCart_ReturnsFalseWhenNoPostData(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
Functions\expect('wc_add_notice')->once();
|
||||
|
||||
// No $_POST['composable_products'] at all
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testAddCartItemData_AddsSelectionsForComposable(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$_POST['composable_products'] = ['101', '102'];
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->add_cart_item_data([], 1);
|
||||
|
||||
$this->assertArrayHasKey('composable_products', $result);
|
||||
$this->assertSame([101, 102], $result['composable_products']);
|
||||
$this->assertArrayHasKey('unique_key', $result);
|
||||
}
|
||||
|
||||
public function testAddCartItemData_PassesThroughForNonComposable(): void
|
||||
{
|
||||
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->add_cart_item_data(['existing' => 'data'], 1);
|
||||
|
||||
$this->assertSame(['existing' => 'data'], $result);
|
||||
}
|
||||
|
||||
public function testGetCartItemFromSession_RestoresComposableProducts(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->get_cart_item_from_session(
|
||||
['data' => 'test'],
|
||||
['composable_products' => [101, 102]]
|
||||
);
|
||||
|
||||
$this->assertSame([101, 102], $result['composable_products']);
|
||||
}
|
||||
|
||||
public function testGetCartItemFromSession_PassesThroughWithoutComposableData(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->get_cart_item_from_session(
|
||||
['data' => 'test'],
|
||||
[]
|
||||
);
|
||||
|
||||
$this->assertArrayNotHasKey('composable_products', $result);
|
||||
}
|
||||
|
||||
public function testDisplayCartItemData_FormatsProductNames(): void
|
||||
{
|
||||
$mock1 = $this->createProductMock(['get_name' => 'Product A']);
|
||||
$mock2 = $this->createProductMock(['get_name' => 'Product B']);
|
||||
|
||||
Functions\expect('wc_get_product')
|
||||
->andReturnUsing(function ($id) use ($mock1, $mock2) {
|
||||
return match ($id) {
|
||||
101 => $mock1,
|
||||
102 => $mock2,
|
||||
default => false,
|
||||
};
|
||||
});
|
||||
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->display_cart_item_data(
|
||||
[],
|
||||
['composable_products' => [101, 102]]
|
||||
);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertStringContainsString('Product A', $result[0]['value']);
|
||||
$this->assertStringContainsString('Product B', $result[0]['value']);
|
||||
}
|
||||
|
||||
public function testDisplayCartItemData_ReturnsEmptyForNonComposable(): void
|
||||
{
|
||||
$handler = new CartHandler();
|
||||
$result = $handler->display_cart_item_data([], []);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
}
|
||||
72
tests/Unit/PluginTest.php
Normal file
72
tests/Unit/PluginTest.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\Plugin;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class PluginTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Reset the singleton instance between tests
|
||||
$reflection = new \ReflectionClass(Plugin::class);
|
||||
$property = $reflection->getProperty('instance');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
|
||||
public function testInstance_ReturnsSingleton(): void
|
||||
{
|
||||
$instance1 = Plugin::instance();
|
||||
$instance2 = Plugin::instance();
|
||||
|
||||
$this->assertSame($instance1, $instance2);
|
||||
}
|
||||
|
||||
public function testInstance_ReturnsPluginClass(): void
|
||||
{
|
||||
$instance = Plugin::instance();
|
||||
|
||||
$this->assertInstanceOf(Plugin::class, $instance);
|
||||
}
|
||||
|
||||
public function testAddProductType_AddsComposableToTypes(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
$types = $plugin->add_product_type([]);
|
||||
|
||||
$this->assertArrayHasKey('composable', $types);
|
||||
}
|
||||
|
||||
public function testProductClass_ReturnsCustomClassForComposable(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
$class = $plugin->product_class('WC_Product', 'composable');
|
||||
|
||||
$this->assertSame('Magdev\WcComposableProduct\ProductType', $class);
|
||||
}
|
||||
|
||||
public function testProductClass_PassesThroughForOtherTypes(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
$class = $plugin->product_class('WC_Product', 'simple');
|
||||
|
||||
$this->assertSame('WC_Product', $class);
|
||||
}
|
||||
|
||||
public function testGetTwig_ReturnsTwigEnvironment(): void
|
||||
{
|
||||
$plugin = Plugin::instance();
|
||||
|
||||
$this->assertInstanceOf(\Twig\Environment::class, $plugin->get_twig());
|
||||
}
|
||||
}
|
||||
218
tests/Unit/ProductTypeTest.php
Normal file
218
tests/Unit/ProductTypeTest.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* ProductType Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\ProductType;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class ProductTypeTest extends TestCase
|
||||
{
|
||||
private function createProductType(array $meta = []): ProductType
|
||||
{
|
||||
$product = new ProductType();
|
||||
foreach ($meta as $key => $value) {
|
||||
$product->update_meta_data($key, $value);
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
public function testGetType_ReturnsComposable(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame('composable', $product->get_type());
|
||||
}
|
||||
|
||||
public function testIsPurchasable_ReturnsTrue(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$this->assertTrue($product->is_purchasable());
|
||||
}
|
||||
|
||||
public function testIsSoldIndividually_ReturnsTrue(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$this->assertTrue($product->is_sold_individually());
|
||||
}
|
||||
|
||||
public function testGetSelectionLimit_UsesProductMeta(): void
|
||||
{
|
||||
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$product = $this->createProductType(['_composable_selection_limit' => '3']);
|
||||
$this->assertSame(3, $product->get_selection_limit());
|
||||
}
|
||||
|
||||
public function testGetSelectionLimit_FallsBackToGlobalDefault(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_default_limit', 5)
|
||||
->andReturn(7);
|
||||
|
||||
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame(7, $product->get_selection_limit());
|
||||
}
|
||||
|
||||
public function testGetSelectionLimit_FallsBackToHardDefault(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_default_limit', 5)
|
||||
->andReturn(5);
|
||||
|
||||
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||
return abs((int) $val);
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame(5, $product->get_selection_limit());
|
||||
}
|
||||
|
||||
public function testGetPricingMode_UsesProductMeta(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
|
||||
$this->assertSame('fixed', $product->get_pricing_mode());
|
||||
}
|
||||
|
||||
public function testGetPricingMode_FallsBackToGlobalDefault(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_default_pricing', 'sum')
|
||||
->andReturn('sum');
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertSame('sum', $product->get_pricing_mode());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_PerProductYes(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_include_unpublished' => 'yes']);
|
||||
$this->assertTrue($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_PerProductNo(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_include_unpublished' => 'no']);
|
||||
$this->assertFalse($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_FallsBackToGlobalYes(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_include_unpublished', 'no')
|
||||
->andReturn('yes');
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertTrue($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testShouldIncludeUnpublished_FallsBackToGlobalNo(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('wc_composable_include_unpublished', 'no')
|
||||
->andReturn('no');
|
||||
|
||||
$product = $this->createProductType();
|
||||
$this->assertFalse($product->should_include_unpublished());
|
||||
}
|
||||
|
||||
public function testGetSelectionCriteria_DefaultsToCategory(): void
|
||||
{
|
||||
$product = $this->createProductType();
|
||||
$criteria = $product->get_selection_criteria();
|
||||
|
||||
$this->assertSame('category', $criteria['type']);
|
||||
$this->assertSame([], $criteria['categories']);
|
||||
$this->assertSame([], $criteria['tags']);
|
||||
$this->assertSame('', $criteria['skus']);
|
||||
}
|
||||
|
||||
public function testGetSelectionCriteria_UsesProductMeta(): void
|
||||
{
|
||||
$product = $this->createProductType([
|
||||
'_composable_criteria_type' => 'tag',
|
||||
'_composable_tags' => [5, 10],
|
||||
]);
|
||||
$criteria = $product->get_selection_criteria();
|
||||
|
||||
$this->assertSame('tag', $criteria['type']);
|
||||
$this->assertSame([5, 10], $criteria['tags']);
|
||||
}
|
||||
|
||||
public function testCalculateComposedPrice_FixedMode(): void
|
||||
{
|
||||
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
|
||||
|
||||
// Set regular price via the stub's data property
|
||||
$reflection = new \ReflectionClass($product);
|
||||
$dataProp = $reflection->getProperty('data');
|
||||
$dataProp->setAccessible(true);
|
||||
$data = $dataProp->getValue($product);
|
||||
$data['regular_price'] = '25.00';
|
||||
$dataProp->setValue($product, $data);
|
||||
|
||||
$price = $product->calculate_composed_price([101, 102]);
|
||||
$this->assertSame(25.0, $price);
|
||||
}
|
||||
|
||||
public function testCalculateComposedPrice_SumMode(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->with('wc_composable_default_pricing', 'sum')
|
||||
->andReturn('sum');
|
||||
|
||||
$mock1 = $this->createProductMock(['get_price' => '5.00']);
|
||||
$mock2 = $this->createProductMock(['get_price' => '7.50']);
|
||||
|
||||
Functions\expect('wc_get_product')
|
||||
->andReturnUsing(function ($id) use ($mock1, $mock2) {
|
||||
return match ($id) {
|
||||
101 => $mock1,
|
||||
102 => $mock2,
|
||||
default => false,
|
||||
};
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$price = $product->calculate_composed_price([101, 102]);
|
||||
|
||||
$this->assertSame(12.5, $price);
|
||||
}
|
||||
|
||||
public function testCalculateComposedPrice_SumMode_SkipsInvalidProducts(): void
|
||||
{
|
||||
Functions\expect('get_option')
|
||||
->with('wc_composable_default_pricing', 'sum')
|
||||
->andReturn('sum');
|
||||
|
||||
$mock1 = $this->createProductMock(['get_price' => '5.00']);
|
||||
|
||||
Functions\expect('wc_get_product')
|
||||
->andReturnUsing(function ($id) use ($mock1) {
|
||||
return match ($id) {
|
||||
101 => $mock1,
|
||||
default => false,
|
||||
};
|
||||
});
|
||||
|
||||
$product = $this->createProductType();
|
||||
$price = $product->calculate_composed_price([101, 999]);
|
||||
|
||||
$this->assertSame(5.0, $price);
|
||||
}
|
||||
}
|
||||
226
tests/Unit/StockManagerTest.php
Normal file
226
tests/Unit/StockManagerTest.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
/**
|
||||
* StockManager Tests
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||
|
||||
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||
use Magdev\WcComposableProduct\StockManager;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class StockManagerTest extends TestCase
|
||||
{
|
||||
private StockManager $manager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->manager = new StockManager();
|
||||
}
|
||||
|
||||
// --- validate_stock_availability ---
|
||||
|
||||
public function testValidateStock_ReturnsTrueWhenAllInStock(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => false,
|
||||
'is_in_stock' => true,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
Functions\expect('wc_get_product')->with(2)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1, 2]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateStock_ReturnsTrueWhenNotManagingStock(): void
|
||||
{
|
||||
$mock = $this->createProductMock(['managing_stock' => false]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateStock_ReturnsErrorForOutOfStock(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'is_in_stock' => false,
|
||||
'get_name' => 'Widget',
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1]);
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringContainsString('Widget', $result);
|
||||
}
|
||||
|
||||
public function testValidateStock_ReturnsErrorForInsufficientQuantity(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'is_in_stock' => true,
|
||||
'get_stock_quantity' => 1,
|
||||
'get_name' => 'Widget',
|
||||
'backorders_allowed' => false,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1], 5);
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringContainsString('Widget', $result);
|
||||
}
|
||||
|
||||
public function testValidateStock_PassesWhenBackordersAllowed(): void
|
||||
{
|
||||
// When stock_quantity is null the insufficient-stock check is skipped,
|
||||
// and the backorders_allowed() branch is reached.
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'is_in_stock' => true,
|
||||
'get_stock_quantity' => null,
|
||||
'backorders_allowed' => true,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([1], 5);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testValidateStock_SkipsNullProducts(): void
|
||||
{
|
||||
Functions\expect('wc_get_product')->with(999)->andReturn(false);
|
||||
|
||||
$result = $this->manager->validate_stock_availability([999]);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
// --- get_product_stock_info ---
|
||||
|
||||
public function testGetProductStockInfo_ReturnsCorrectStructure(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'is_in_stock' => true,
|
||||
'get_stock_quantity' => 10,
|
||||
'backorders_allowed' => false,
|
||||
'get_stock_status' => 'instock',
|
||||
'managing_stock' => true,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(1);
|
||||
|
||||
$this->assertTrue($info['in_stock']);
|
||||
$this->assertSame(10, $info['stock_quantity']);
|
||||
$this->assertFalse($info['backorders_allowed']);
|
||||
$this->assertSame('instock', $info['stock_status']);
|
||||
$this->assertTrue($info['managing_stock']);
|
||||
$this->assertTrue($info['has_enough_stock']);
|
||||
}
|
||||
|
||||
public function testGetProductStockInfo_ReturnsFallbackForInvalidProduct(): void
|
||||
{
|
||||
Functions\expect('wc_get_product')->with(999)->andReturn(false);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(999);
|
||||
|
||||
$this->assertFalse($info['in_stock']);
|
||||
$this->assertSame(0, $info['stock_quantity']);
|
||||
$this->assertFalse($info['backorders_allowed']);
|
||||
$this->assertSame('outofstock', $info['stock_status']);
|
||||
}
|
||||
|
||||
public function testGetProductStockInfo_HasEnoughStockTrueWhenNotManaging(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => false,
|
||||
'get_stock_quantity' => null,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(1, 100);
|
||||
$this->assertTrue($info['has_enough_stock']);
|
||||
}
|
||||
|
||||
public function testGetProductStockInfo_HasEnoughStockFalseWhenInsufficient(): void
|
||||
{
|
||||
$mock = $this->createProductMock([
|
||||
'managing_stock' => true,
|
||||
'get_stock_quantity' => 2,
|
||||
]);
|
||||
|
||||
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||
|
||||
$info = $this->manager->get_product_stock_info(1, 5);
|
||||
$this->assertFalse($info['has_enough_stock']);
|
||||
}
|
||||
|
||||
// --- prevent_composable_stock_reduction ---
|
||||
|
||||
public function testPreventStockReduction_ReturnsFalseForComposableItem(): void
|
||||
{
|
||||
$productMock = $this->createProductMock(['get_type' => 'composable']);
|
||||
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldReceive('get_product')->andReturn($productMock);
|
||||
|
||||
$orderMock = \Mockery::mock('WC_Order');
|
||||
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
|
||||
|
||||
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testPreventStockReduction_PassesThroughForNonComposable(): void
|
||||
{
|
||||
$productMock = $this->createProductMock(['get_type' => 'simple']);
|
||||
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldReceive('get_product')->andReturn($productMock);
|
||||
|
||||
$orderMock = \Mockery::mock('WC_Order');
|
||||
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
|
||||
|
||||
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
// --- store_selected_products_in_order ---
|
||||
|
||||
public function testStoreSelectedProducts_AddsMetaWhenPresent(): void
|
||||
{
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldReceive('add_meta_data')
|
||||
->once()
|
||||
->with('_composable_products', [1, 2], true);
|
||||
|
||||
$this->manager->store_selected_products_in_order(
|
||||
$itemMock,
|
||||
'cart_key',
|
||||
['composable_products' => [1, 2]]
|
||||
);
|
||||
}
|
||||
|
||||
public function testStoreSelectedProducts_DoesNothingWithoutData(): void
|
||||
{
|
||||
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||
$itemMock->shouldNotReceive('add_meta_data');
|
||||
|
||||
$this->manager->store_selected_products_in_order(
|
||||
$itemMock,
|
||||
'cart_key',
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
33
tests/bootstrap.php
Normal file
33
tests/bootstrap.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* PHPUnit Bootstrap
|
||||
*
|
||||
* Sets up WooCommerce stubs, plugin constants, and the Composer autoloader
|
||||
* for running unit tests without a full WordPress installation.
|
||||
*
|
||||
* @package Magdev\WcComposableProduct\Tests
|
||||
*/
|
||||
|
||||
// Composer autoloader (loads Brain Monkey, Mockery, plugin classes)
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
// Define WordPress constants that the plugin expects
|
||||
defined('ABSPATH') || define('ABSPATH', '/tmp/wordpress/');
|
||||
defined('WP_DEBUG') || define('WP_DEBUG', true);
|
||||
defined('DOING_AJAX') || define('DOING_AJAX', false);
|
||||
|
||||
// Define plugin constants
|
||||
defined('WC_COMPOSABLE_PRODUCT_VERSION') || define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1');
|
||||
defined('WC_COMPOSABLE_PRODUCT_FILE') || define('WC_COMPOSABLE_PRODUCT_FILE', dirname(__DIR__) . '/wc-composable-product.php');
|
||||
defined('WC_COMPOSABLE_PRODUCT_PATH') || define('WC_COMPOSABLE_PRODUCT_PATH', dirname(__DIR__) . '/');
|
||||
defined('WC_COMPOSABLE_PRODUCT_URL') || define('WC_COMPOSABLE_PRODUCT_URL', 'https://example.com/wp-content/plugins/wc-composable-product/');
|
||||
defined('WC_COMPOSABLE_PRODUCT_BASENAME') || define('WC_COMPOSABLE_PRODUCT_BASENAME', 'wc-composable-product/wc-composable-product.php');
|
||||
|
||||
// Load WooCommerce class stubs (parent before child)
|
||||
require_once __DIR__ . '/stubs/class-wc-data.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-product.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-settings-page.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-order.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-order-item-product.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-cart.php';
|
||||
require_once __DIR__ . '/stubs/class-wc-admin-settings.php';
|
||||
11
tests/stubs/class-wc-admin-settings.php
Normal file
11
tests/stubs/class-wc-admin-settings.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Admin_Settings stub for unit testing.
|
||||
*/
|
||||
class WC_Admin_Settings {
|
||||
public static function output_fields($options) {
|
||||
}
|
||||
|
||||
public static function save_fields($options) {
|
||||
}
|
||||
}
|
||||
15
tests/stubs/class-wc-cart.php
Normal file
15
tests/stubs/class-wc-cart.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Cart stub for unit testing.
|
||||
*/
|
||||
class WC_Cart {
|
||||
protected $cart_contents = [];
|
||||
|
||||
public function get_cart() {
|
||||
return $this->cart_contents;
|
||||
}
|
||||
|
||||
public function set_cart_contents($contents) {
|
||||
$this->cart_contents = $contents;
|
||||
}
|
||||
}
|
||||
35
tests/stubs/class-wc-data.php
Normal file
35
tests/stubs/class-wc-data.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Data stub for unit testing.
|
||||
*/
|
||||
class WC_Data {
|
||||
protected $id = 0;
|
||||
protected $data = [];
|
||||
protected $meta_data = [];
|
||||
|
||||
public function __construct($id = 0) {
|
||||
if (is_numeric($id) && $id > 0) {
|
||||
$this->id = (int) $id;
|
||||
}
|
||||
}
|
||||
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function set_id($id) {
|
||||
$this->id = (int) $id;
|
||||
}
|
||||
|
||||
public function get_meta($key, $single = true, $context = 'view') {
|
||||
return $this->meta_data[$key] ?? ($single ? '' : []);
|
||||
}
|
||||
|
||||
public function update_meta_data($key, $value, $meta_id = 0) {
|
||||
$this->meta_data[$key] = $value;
|
||||
}
|
||||
|
||||
public function save() {
|
||||
return $this->get_id();
|
||||
}
|
||||
}
|
||||
28
tests/stubs/class-wc-order-item-product.php
Normal file
28
tests/stubs/class-wc-order-item-product.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Order_Item_Product stub for unit testing.
|
||||
*/
|
||||
class WC_Order_Item_Product extends WC_Data {
|
||||
protected $product = null;
|
||||
protected $quantity = 1;
|
||||
|
||||
public function get_product() {
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function set_product($product) {
|
||||
$this->product = $product;
|
||||
}
|
||||
|
||||
public function get_quantity() {
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function set_quantity($quantity) {
|
||||
$this->quantity = $quantity;
|
||||
}
|
||||
|
||||
public function add_meta_data($key, $value, $unique = false) {
|
||||
$this->meta_data[$key] = $value;
|
||||
}
|
||||
}
|
||||
24
tests/stubs/class-wc-order.php
Normal file
24
tests/stubs/class-wc-order.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Order stub for unit testing.
|
||||
*/
|
||||
class WC_Order extends WC_Data {
|
||||
protected $items = [];
|
||||
protected $order_notes = [];
|
||||
|
||||
public function get_items($type = '') {
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function set_items($items) {
|
||||
$this->items = $items;
|
||||
}
|
||||
|
||||
public function add_order_note($note) {
|
||||
$this->order_notes[] = $note;
|
||||
}
|
||||
|
||||
public function get_order_notes() {
|
||||
return $this->order_notes;
|
||||
}
|
||||
}
|
||||
88
tests/stubs/class-wc-product.php
Normal file
88
tests/stubs/class-wc-product.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Product stub for unit testing.
|
||||
*/
|
||||
class WC_Product extends WC_Data {
|
||||
protected $supports = [];
|
||||
protected $data = [
|
||||
'name' => '',
|
||||
'price' => '',
|
||||
'regular_price' => '',
|
||||
'status' => 'publish',
|
||||
];
|
||||
|
||||
public function get_type() {
|
||||
return 'simple';
|
||||
}
|
||||
|
||||
public function get_name($context = 'view') {
|
||||
return $this->data['name'] ?? '';
|
||||
}
|
||||
|
||||
public function get_price($context = 'view') {
|
||||
return $this->data['price'] ?? '';
|
||||
}
|
||||
|
||||
public function get_regular_price($context = 'view') {
|
||||
return $this->data['regular_price'] ?? '';
|
||||
}
|
||||
|
||||
public function get_price_html() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function get_permalink() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function get_image_id($context = 'view') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function get_stock_quantity($context = 'view') {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_stock_status($context = 'view') {
|
||||
return 'instock';
|
||||
}
|
||||
|
||||
public function get_children() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function is_type($type) {
|
||||
return $this->get_type() === $type;
|
||||
}
|
||||
|
||||
public function is_purchasable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function is_in_stock() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function managing_stock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function backorders_allowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function is_sold_individually() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function set_price($price) {
|
||||
$this->data['price'] = $price;
|
||||
}
|
||||
|
||||
public function set_stock_quantity($quantity) {
|
||||
}
|
||||
|
||||
public function supports($feature) {
|
||||
return in_array($feature, $this->supports, true);
|
||||
}
|
||||
}
|
||||
29
tests/stubs/class-wc-settings-page.php
Normal file
29
tests/stubs/class-wc-settings-page.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal WC_Settings_Page stub for unit testing.
|
||||
*/
|
||||
class WC_Settings_Page {
|
||||
protected $id = '';
|
||||
protected $label = '';
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function get_label() {
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function get_settings() {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function output() {
|
||||
}
|
||||
|
||||
public function save() {
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* Plugin Name: WooCommerce Composable Products
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-composable-product
|
||||
* Description: Create composable products where customers select a limited number of items from a configurable set
|
||||
* Version: 1.3.0
|
||||
* Version: 1.3.1
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL v3 or later
|
||||
@@ -20,7 +20,7 @@
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
// Define plugin constants
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.0');
|
||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1');
|
||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user