| 1 |
<?php |
| 2 |
|
| 3 |
namespace SyncBasalam\Admin\Product\Data\Services { |
| 4 |
function syncBasalamSettings() |
| 5 |
{ |
| 6 |
return $GLOBALS['attribute_suffix_test_settings'] ?? \syncBasalamSettings(); |
| 7 |
} |
| 8 |
|
| 9 |
function wc_attribute_label($name) |
| 10 |
{ |
| 11 |
return $GLOBALS['attribute_suffix_test_labels'][$name] ?? $name; |
| 12 |
} |
| 13 |
|
| 14 |
function wc_get_product_terms($productId, $taxonomy, $args) |
| 15 |
{ |
| 16 |
$GLOBALS['attribute_suffix_test_term_calls'][] = [$productId, $taxonomy, $args]; |
| 17 |
return $GLOBALS['attribute_suffix_test_terms'][$taxonomy] ?? []; |
| 18 |
} |
| 19 |
} |
| 20 |
|
| 21 |
namespace SyncBasalam\Admin\Components { |
| 22 |
function syncBasalamSettings() |
| 23 |
{ |
| 24 |
return $GLOBALS['attribute_suffix_test_settings'] ?? \syncBasalamSettings(); |
| 25 |
} |
| 26 |
|
| 27 |
function esc_attr($value) |
| 28 |
{ |
| 29 |
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); |
| 30 |
} |
| 31 |
} |
| 32 |
|
| 33 |
namespace SyncBasalam\Tests\Admin { |
| 34 |
use PHPUnit\Framework\TestCase; |
| 35 |
use ReflectionClass; |
| 36 |
use ReflectionProperty; |
| 37 |
use SyncBasalam\Admin\Components\SettingPageComponents; |
| 38 |
use SyncBasalam\Admin\Product\Data\Handlers\SimpleProductHandler; |
| 39 |
use SyncBasalam\Admin\Product\Data\Handlers\VariableProductHandler; |
| 40 |
use SyncBasalam\Admin\Product\Data\Services\AttributeService; |
| 41 |
use SyncBasalam\Admin\Settings\SettingsConfig; |
| 42 |
|
| 43 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Settings/SettingsConfig.php'; |
| 44 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Components/SettingPageComponents.php'; |
| 45 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Product/Data/Services/MobileDataHandler.php'; |
| 46 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Product/Data/Services/GoldDataHandler.php'; |
| 47 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Product/Data/Services/AttributeService.php'; |
| 48 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Product/Data/Handlers/ProductDataHandlerInterface.php'; |
| 49 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Product/Data/Handlers/SimpleProductHandler.php'; |
| 50 |
require_once dirname(__DIR__, 2) . '/includes/Admin/Product/Data/Handlers/VariableProductHandler.php'; |
| 51 |
|
| 52 |
class ProductAttributeSuffixTest extends TestCase |
| 53 |
{ |
| 54 |
private array $settings; |
| 55 |
|
| 56 |
protected function setUp(): void |
| 57 |
{ |
| 58 |
$this->configure('ناشر, نویسنده'); |
| 59 |
$GLOBALS['attribute_suffix_test_labels'] = []; |
| 60 |
$GLOBALS['attribute_suffix_test_terms'] = []; |
| 61 |
$GLOBALS['attribute_suffix_test_term_calls'] = []; |
| 62 |
} |
| 63 |
|
| 64 |
protected function tearDown(): void |
| 65 |
{ |
| 66 |
unset( |
| 67 |
$GLOBALS['attribute_suffix_test_settings'], |
| 68 |
$GLOBALS['attribute_suffix_test_labels'], |
| 69 |
$GLOBALS['attribute_suffix_test_terms'], |
| 70 |
$GLOBALS['attribute_suffix_test_term_calls'] |
| 71 |
); |
| 72 |
} |
| 73 |
|
| 74 |
/** @dataProvider suffixCases */ |
| 75 |
public function testBuildsSuffixFromConfiguredAttributeLabels($configured, array $attributes, ?string $expected): void |
| 76 |
{ |
| 77 |
$this->configure($configured); |
| 78 |
|
| 79 |
self::assertSame($expected, (new AttributeService())->getAttributeSuffix($this->product($attributes))); |
| 80 |
} |
| 81 |
|
| 82 |
public function suffixCases(): array |
| 83 |
{ |
| 84 |
$book = ['ناشر' => ['نشر چش� |
| 85 |
ه'], 'نویسنده' => ['صادق هدایت']]; |
| 86 |
|
| 87 |
return [ |
| 88 |
'existing single attribute' => ['ناشر', $book, 'نشر چش� |
| 89 |
ه'], |
| 90 |
'two attributes with English comma' => ['ناشر, نویسنده', $book, 'نشر چش� |
| 91 |
ه, صادق هدایت'], |
| 92 |
'two attributes with Persian comma' => ['ناشر، نویسنده', $book, 'نشر چش� |
| 93 |
ه, صادق هدایت'], |
| 94 |
'configured order differs from product order' => ['نویسنده, ناشر', $book, 'صادق هدایت, نشر چش� |
| 95 |
ه'], |
| 96 |
'whitespace around names and values' => [" \tناشر , نویسنده\n", [' ناشر ' => [' نشر چش� |
| 97 |
ه '], 'نویسنده' => [' صادق هدایت ']], 'نشر چش� |
| 98 |
ه, صادق هدایت'], |
| 99 |
'empty entries and mixed separators' => [', ناشر، , نویسنده،', $book, 'نشر چش� |
| 100 |
ه, صادق هدایت'], |
| 101 |
'duplicate names do not repeat values' => ['ناشر, ناشر، نویسنده,ناشر', $book, 'نشر چش� |
| 102 |
ه, صادق هدایت'], |
| 103 |
'first attribute missing' => ['� |
| 104 |
ترج� |
| 105 |
, ناشر', $book, 'نشر چش� |
| 106 |
ه'], |
| 107 |
'second attribute missing' => ['ناشر, � |
| 108 |
ترج� |
| 109 |
', $book, 'نشر چش� |
| 110 |
ه'], |
| 111 |
'all attributes missing' => ['� |
| 112 |
ترج� |
| 113 |
, زبان', $book, null], |
| 114 |
'first attribute empty' => ['ناشر, نویسنده', ['ناشر' => [], 'نویسنده' => ['صادق هدایت']], 'صادق هدایت'], |
| 115 |
'second attribute empty' => ['ناشر, نویسنده', ['ناشر' => ['نشر چش� |
| 116 |
ه'], 'نویسنده' => ['']], 'نشر چش� |
| 117 |
ه'], |
| 118 |
'whitespace value skipped' => ['ناشر, نویسنده', ['ناشر' => [" \t"], 'نویسنده' => ['صادق هدایت']], 'صادق هدایت'], |
| 119 |
'all values empty' => ['ناشر, نویسنده', ['ناشر' => [''], 'نویسنده' => []], null], |
| 120 |
'no product attributes' => ['ناشر, نویسنده', [], null], |
| 121 |
'empty setting' => ['', $book, null], |
| 122 |
'unset setting' => [null, $book, null], |
| 123 |
'only separators and whitespace' => [" , ، \t,", $book, null], |
| 124 |
'multiple values in one attribute' => ['ناشر, نویسنده', ['ناشر' => ['نشر چش� |
| 125 |
ه'], 'نویسنده' => ['صادق هدایت', 'بزرگ علوی']], 'نشر چش� |
| 126 |
ه, صادق هدایت, بزرگ علوی'], |
| 127 |
'zero is a valid attribute value' => ['ش� |
| 128 |
اره, ناشر', ['ش� |
| 129 |
اره' => ['0'], 'ناشر' => ['نشر چش� |
| 130 |
ه']], '0, نشر چش� |
| 131 |
ه'], |
| 132 |
'zero is a valid attribute label' => ['0, ناشر', ['0' => ['ویژگی صفر'], 'ناشر' => ['نشر چش� |
| 133 |
ه']], 'ویژگی صفر, نشر چش� |
| 134 |
ه'], |
| 135 |
'identical values belong to distinct attributes' => ['ناشر, نویسنده', ['ناشر' => ['� |
| 136 |
شترک'], 'نویسنده' => ['� |
| 137 |
شترک']], '� |
| 138 |
شترک, � |
| 139 |
شترک'], |
| 140 |
'three names and mixed commas' => ['ناشر، نویسنده, زبان', $book + ['زبان' => ['فارسی']], 'نشر چش� |
| 141 |
ه, صادق هدایت, فارسی'], |
| 142 |
'unselected attributes are excluded' => ['ناشر, نویسنده', ['زبان' => ['فارسی']] + $book, 'نشر چش� |
| 143 |
ه, صادق هدایت'], |
| 144 |
'labels remain exact matches' => ['ناش, نویسنده', $book, 'صادق هدایت'], |
| 145 |
]; |
| 146 |
} |
| 147 |
|
| 148 |
/** @dataProvider disabledSettings */ |
| 149 |
public function testDisabledFeatureDoesNotReadProductAttributes($enabled): void |
| 150 |
{ |
| 151 |
$this->configure('ناشر, نویسنده', $enabled); |
| 152 |
$product = new class { |
| 153 |
public function get_attributes() |
| 154 |
{ |
| 155 |
throw new \RuntimeException('Disabled suffixes must not read attributes.'); |
| 156 |
} |
| 157 |
}; |
| 158 |
|
| 159 |
self::assertNull((new AttributeService())->getAttributeSuffix($product)); |
| 160 |
} |
| 161 |
|
| 162 |
public function disabledSettings(): array |
| 163 |
{ |
| 164 |
return [['no'], [false], [null], [''], [true]]; |
| 165 |
} |
| 166 |
|
| 167 |
public function testUsesGlobalTaxonomyLabelsAndProductTermNamesAlongsideCustomAttributes(): void |
| 168 |
{ |
| 169 |
$GLOBALS['attribute_suffix_test_labels']['pa_publisher'] = 'ناشر'; |
| 170 |
$GLOBALS['attribute_suffix_test_terms']['pa_publisher'] = ['نشر چش� |
| 171 |
ه', 'نشر نی']; |
| 172 |
$product = $this->product(['نویسنده' => ['صادق هدایت'], 'pa_publisher' => []], ['pa_publisher']); |
| 173 |
|
| 174 |
self::assertSame('نشر چش� |
| 175 |
ه, نشر نی, صادق هدایت', (new AttributeService())->getAttributeSuffix($product)); |
| 176 |
self::assertSame([[42, 'pa_publisher', ['fields' => 'names']]], $GLOBALS['attribute_suffix_test_term_calls']); |
| 177 |
} |
| 178 |
|
| 179 |
public function testCombinesTwoGlobalAttributesInConfiguredOrder(): void |
| 180 |
{ |
| 181 |
$GLOBALS['attribute_suffix_test_labels'] = ['pa_author' => 'نویسنده', 'pa_publisher' => 'ناشر']; |
| 182 |
$GLOBALS['attribute_suffix_test_terms'] = ['pa_author' => ['صادق هدایت'], 'pa_publisher' => ['نشر چش� |
| 183 |
ه']]; |
| 184 |
$product = $this->product(['pa_author' => [], 'pa_publisher' => []], ['pa_author', 'pa_publisher']); |
| 185 |
|
| 186 |
self::assertSame('نشر چش� |
| 187 |
ه, صادق هدایت', (new AttributeService())->getAttributeSuffix($product)); |
| 188 |
} |
| 189 |
|
| 190 |
public function testSkipsTaxonomyAttributesWithNoAssignedTerms(): void |
| 191 |
{ |
| 192 |
$GLOBALS['attribute_suffix_test_labels']['pa_publisher'] = 'ناشر'; |
| 193 |
$product = $this->product(['pa_publisher' => [], 'نویسنده' => ['صادق هدایت']], ['pa_publisher']); |
| 194 |
|
| 195 |
self::assertSame('صادق هدایت', (new AttributeService())->getAttributeSuffix($product)); |
| 196 |
} |
| 197 |
|
| 198 |
/** @dataProvider titleCases */ |
| 199 |
public function testGeneratedTitlesPreserveExistingFormattingForBothProductHandlers( |
| 200 |
string $configured, array $attributes, string $prefix, string $suffix, string $baseName, string $expected |
| 201 |
): void { |
| 202 |
$this->configure($configured); |
| 203 |
$this->settings[SettingsConfig::PRODUCT_PREFIX_TITLE] = $prefix; |
| 204 |
$this->settings[SettingsConfig::PRODUCT_SUFFIX_TITLE] = $suffix; |
| 205 |
|
| 206 |
foreach ([SimpleProductHandler::class, VariableProductHandler::class] as $handlerClass) { |
| 207 |
self::assertSame($expected, $this->handler($handlerClass)->getName($this->product($attributes, [], $baseName))); |
| 208 |
} |
| 209 |
} |
| 210 |
|
| 211 |
public function titleCases(): array |
| 212 |
{ |
| 213 |
$book = ['ناشر' => ['نشر چش� |
| 214 |
ه'], 'نویسنده' => ['صادق هدایت']]; |
| 215 |
|
| 216 |
return [ |
| 217 |
'both values inside one pair of parentheses' => ['ناشر, نویسنده', $book, '', '', 'کتاب', 'کتاب (نشر چش� |
| 218 |
ه, صادق هدایت)'], |
| 219 |
'static prefix and suffix remain' => ['ناشر, نویسنده', $book, 'فروشگاه', 'اصل', 'کتاب', 'فروشگاه کتاب اصل (نشر چش� |
| 220 |
ه, صادق هدایت)'], |
| 221 |
'single setting remains compatible' => ['ناشر', $book, '', '', 'کتاب', 'کتاب (نشر چش� |
| 222 |
ه)'], |
| 223 |
'one missing attribute has no dangling separator' => ['� |
| 224 |
ترج� |
| 225 |
, ناشر', $book, '', '', 'کتاب', 'کتاب (نشر چش� |
| 226 |
ه)'], |
| 227 |
'no matches has no empty parentheses' => ['� |
| 228 |
ترج� |
| 229 |
, زبان', $book, 'فروشگاه', 'اصل', 'کتاب', 'فروشگاه کتاب اصل'], |
| 230 |
'empty setting leaves title alone' => ['', $book, '', '', 'کتاب', 'کتاب'], |
| 231 |
'standalone zero value is kept' => ['ش� |
| 232 |
اره', ['ش� |
| 233 |
اره' => ['0']], '', '', 'کالا', 'کالا (0)'], |
| 234 |
'Persian title is capped at 120 characters' => ['ناشر, نویسنده', $book, '', '', str_repeat('ک', 115), str_repeat('ک', 115) . ' (نشر'], |
| 235 |
]; |
| 236 |
} |
| 237 |
|
| 238 |
public function testDisabledSuffixDoesNotChangeGeneratedTitle(): void |
| 239 |
{ |
| 240 |
$this->configure('ناشر, نویسنده', 'no'); |
| 241 |
self::assertSame('کتاب', $this->handler(SimpleProductHandler::class)->getName($this->product(['ناشر' => ['نشر چش� |
| 242 |
ه']]))); |
| 243 |
} |
| 244 |
|
| 245 |
public function testSettingsInputRetainsCommaSeparatedNamesAndShowsTheExample(): void |
| 246 |
{ |
| 247 |
$this->configure('ناشر, نویسنده'); |
| 248 |
ob_start(); |
| 249 |
SettingPageComponents::renderAttributeSuffixPriority(); |
| 250 |
$html = ob_get_clean(); |
| 251 |
|
| 252 |
self::assertStringContainsString('value="ناشر, نویسنده"', $html); |
| 253 |
self::assertStringContainsString('placeholder="� |
| 254 |
ثال: ناشر, نویسنده"', $html); |
| 255 |
self::assertStringNotContainsString('disabled', $html); |
| 256 |
} |
| 257 |
|
| 258 |
public function testDisabledSettingsInputRetainsAndEscapesItsValue(): void |
| 259 |
{ |
| 260 |
$this->configure('ناشر, "نویسنده"', 'no'); |
| 261 |
ob_start(); |
| 262 |
SettingPageComponents::renderAttributeSuffixPriority(); |
| 263 |
$html = ob_get_clean(); |
| 264 |
|
| 265 |
self::assertStringContainsString('value="ناشر, "نویسنده""', $html); |
| 266 |
self::assertStringContainsString('disabled', $html); |
| 267 |
} |
| 268 |
|
| 269 |
private function configure($names, $enabled = 'yes'): void |
| 270 |
{ |
| 271 |
$this->settings = array_merge(SettingsConfig::getDefaultSettings(), [ |
| 272 |
SettingsConfig::PRODUCT_ATTRIBUTE_SUFFIX_ENABLED => $enabled, |
| 273 |
SettingsConfig::PRODUCT_ATTRIBUTE_SUFFIX_PRIORITY => $names, |
| 274 |
]); |
| 275 |
$GLOBALS['attribute_suffix_test_settings'] = new class($this->settings) { |
| 276 |
private array $settings; |
| 277 |
public function __construct(array $settings) { $this->settings = $settings; } |
| 278 |
public function getSettings($key = null) { return $key === null ? $this->settings : ($this->settings[$key] ?? null); } |
| 279 |
}; |
| 280 |
} |
| 281 |
|
| 282 |
private function product(array $attributes, array $taxonomies = [], string $name = 'کتاب') |
| 283 |
{ |
| 284 |
$fixtures = []; |
| 285 |
foreach ($attributes as $label => $options) { |
| 286 |
$fixtures[] = new class((string) $label, $options, in_array($label, $taxonomies, true)) { |
| 287 |
private string $name; |
| 288 |
private array $options; |
| 289 |
private bool $taxonomy; |
| 290 |
public function __construct(string $name, array $options, bool $taxonomy) |
| 291 |
{ |
| 292 |
$this->name = $name; |
| 293 |
$this->options = $options; |
| 294 |
$this->taxonomy = $taxonomy; |
| 295 |
} |
| 296 |
public function is_taxonomy(): bool { return $this->taxonomy; } |
| 297 |
public function get_name(): string { return $this->name; } |
| 298 |
public function get_options(): array { return $this->options; } |
| 299 |
}; |
| 300 |
} |
| 301 |
|
| 302 |
return new class($fixtures, $name) { |
| 303 |
private array $attributes; |
| 304 |
private string $name; |
| 305 |
public function __construct(array $attributes, string $name) { $this->attributes = $attributes; $this->name = $name; } |
| 306 |
public function get_attributes(): array { return $this->attributes; } |
| 307 |
public function get_name(): string { return $this->name; } |
| 308 |
public function get_id(): int { return 42; } |
| 309 |
}; |
| 310 |
} |
| 311 |
|
| 312 |
private function handler(string $class): SimpleProductHandler |
| 313 |
{ |
| 314 |
// Name generation only needs settings and AttributeService; avoid |
| 315 |
// constructing unrelated upload and category API dependencies. |
| 316 |
$handler = (new ReflectionClass($class))->newInstanceWithoutConstructor(); |
| 317 |
foreach (['settings' => $this->settings, 'attributeService' => new AttributeService()] as $name => $value) { |
| 318 |
$property = new ReflectionProperty(SimpleProductHandler::class, $name); |
| 319 |
$property->setAccessible(true); |
| 320 |
$property->setValue($handler, $value); |
| 321 |
} |
| 322 |
return $handler; |
| 323 |
} |
| 324 |
} |
| 325 |
} |
| 326 |
|