ima-claude 2.20.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -9
- package/dist/cli.js +2 -1
- package/package.json +1 -1
- package/plugins/ima-claude/.claude-plugin/plugin.json +2 -2
- package/plugins/ima-claude/agents/explorer.md +29 -15
- package/plugins/ima-claude/agents/implementer.md +58 -13
- package/plugins/ima-claude/agents/memory.md +19 -19
- package/plugins/ima-claude/agents/reviewer.md +84 -34
- package/plugins/ima-claude/agents/tester.md +59 -16
- package/plugins/ima-claude/agents/wp-developer.md +66 -21
- package/plugins/ima-claude/hooks/bootstrap.sh +42 -44
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +14 -17
- package/plugins/ima-claude/hooks/prompt_coach_system.md +10 -12
- package/plugins/ima-claude/personalities/README.md +17 -6
- package/plugins/ima-claude/personalities/enable-efficient.md +61 -0
- package/plugins/ima-claude/personalities/enable-terse.md +71 -0
- package/plugins/ima-claude/skills/agentic-workflows/SKILL.md +35 -71
- package/plugins/ima-claude/skills/architect/SKILL.md +54 -168
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +41 -94
- package/plugins/ima-claude/skills/design-to-code/SKILL.md +43 -78
- package/plugins/ima-claude/skills/discourse/SKILL.md +79 -194
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +41 -103
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +63 -203
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +90 -200
- package/plugins/ima-claude/skills/espocrm/SKILL.md +14 -23
- package/plugins/ima-claude/skills/espocrm-api/SKILL.md +79 -192
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +33 -237
- package/plugins/ima-claude/skills/gh-cli/SKILL.md +26 -65
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +71 -104
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +32 -22
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +18 -23
- package/plugins/ima-claude/skills/ima-copywriting/SKILL.md +68 -179
- package/plugins/ima-claude/skills/ima-doc2pdf/SKILL.md +32 -102
- package/plugins/ima-claude/skills/ima-editorial-scorecard/SKILL.md +38 -63
- package/plugins/ima-claude/skills/ima-editorial-workflow/SKILL.md +69 -114
- package/plugins/ima-claude/skills/ima-email-creator/SKILL.md +16 -22
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +21 -37
- package/plugins/ima-claude/skills/ima-git/SKILL.md +81 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +39 -120
- package/plugins/ima-claude/skills/jquery/SKILL.md +107 -233
- package/plugins/ima-claude/skills/js-fp/SKILL.md +75 -296
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +52 -162
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +47 -270
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +55 -209
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +59 -204
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +19 -32
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +92 -162
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +32 -64
- package/plugins/ima-claude/skills/mcp-gitea/SKILL.md +98 -188
- package/plugins/ima-claude/skills/mcp-github/SKILL.md +60 -124
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +1 -177
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +58 -115
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +32 -87
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +54 -80
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +40 -63
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +75 -116
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +32 -65
- package/plugins/ima-claude/skills/php-fp/SKILL.md +50 -129
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +25 -73
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +103 -463
- package/plugins/ima-claude/skills/playwright/SKILL.md +69 -220
- package/plugins/ima-claude/skills/prompt-starter/SKILL.md +33 -83
- package/plugins/ima-claude/skills/prompt-starter/references/code-review.md +38 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +78 -384
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +54 -255
- package/plugins/ima-claude/skills/quickstart/SKILL.md +7 -11
- package/plugins/ima-claude/skills/rails/SKILL.md +63 -184
- package/plugins/ima-claude/skills/resume-session/SKILL.md +14 -35
- package/plugins/ima-claude/skills/rg/SKILL.md +61 -146
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +66 -163
- package/plugins/ima-claude/skills/save-session/SKILL.md +10 -39
- package/plugins/ima-claude/skills/scorecard/SKILL.md +42 -40
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +42 -71
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +79 -250
- package/plugins/ima-claude/skills/task-master/SKILL.md +11 -31
- package/plugins/ima-claude/skills/task-planner/SKILL.md +44 -153
- package/plugins/ima-claude/skills/task-runner/SKILL.md +61 -143
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +59 -134
- package/plugins/ima-claude/skills/wp-ddev/SKILL.md +38 -120
- package/plugins/ima-claude/skills/wp-local/SKILL.md +26 -108
|
@@ -5,46 +5,28 @@ description: "PHPUnit testing for WordPress plugins with FP principles - fast un
|
|
|
5
5
|
|
|
6
6
|
# PHPUnit for WordPress Plugins
|
|
7
7
|
|
|
8
|
-
Expert guidance for PHPUnit testing in WordPress plugins, emphasizing pure function testing, minimal mocking, and FP principles.
|
|
9
|
-
|
|
10
|
-
## When to Use This Skill
|
|
11
|
-
|
|
12
|
-
- Setting up PHPUnit for new WordPress plugins
|
|
13
|
-
- Debugging silent/hanging test runs
|
|
14
|
-
- Writing testable pure functions
|
|
15
|
-
- Mocking WordPress functions correctly
|
|
16
|
-
- Testing with FP principles
|
|
17
|
-
|
|
18
8
|
## Core Philosophy
|
|
19
9
|
|
|
20
|
-
|
|
10
|
+
Test pure functions, not WordPress integration.
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
| Layer | Approach | Speed |
|
|
13
|
+
|-------|----------|-------|
|
|
14
|
+
| Pure business logic | Unit tests, no WordPress | <100ms |
|
|
15
|
+
| WordPress wrappers | Integration tests, full WP env | Seconds |
|
|
16
|
+
| Mocks | Only what pure functions actually use | Minimal |
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
References: `../php-fp/SKILL.md`, `../php-fp-wordpress/SKILL.md`
|
|
27
19
|
|
|
28
20
|
---
|
|
29
21
|
|
|
30
|
-
## THE TWO CRITICAL SETUP BUGS
|
|
22
|
+
## THE TWO CRITICAL SETUP BUGS
|
|
31
23
|
|
|
32
|
-
###
|
|
24
|
+
### Bug #1: Silent PHPUnit Execution
|
|
33
25
|
|
|
34
|
-
**Symptom**: `composer test` produces
|
|
26
|
+
**Symptom**: `composer test` produces zero output
|
|
35
27
|
|
|
36
|
-
**
|
|
28
|
+
**Cause**: PHPUnit 9.x requires `--testdox` for visible output
|
|
37
29
|
|
|
38
|
-
**❌ BROKEN** (silent execution):
|
|
39
|
-
```json
|
|
40
|
-
{
|
|
41
|
-
"scripts": {
|
|
42
|
-
"test": "phpunit"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**✅ FIXED** (visible output):
|
|
48
30
|
```json
|
|
49
31
|
{
|
|
50
32
|
"scripts": {
|
|
@@ -54,46 +36,25 @@ Expert guidance for PHPUnit testing in WordPress plugins, emphasizing pure funct
|
|
|
54
36
|
}
|
|
55
37
|
```
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### 🐛 Bug #2: Autoload Files Kill Tests
|
|
39
|
+
### Bug #2: Autoload Files Kill Tests
|
|
60
40
|
|
|
61
|
-
**Symptom**: Tests hang or exit silently
|
|
41
|
+
**Symptom**: Tests hang or exit silently
|
|
62
42
|
|
|
63
|
-
**
|
|
43
|
+
**Cause**: Composer autoload runs BEFORE bootstrap defines `ABSPATH`
|
|
64
44
|
|
|
65
|
-
**The Fatal Flow**:
|
|
66
45
|
```
|
|
67
|
-
1. bootstrap.php
|
|
68
|
-
2. Composer
|
|
69
|
-
3.
|
|
70
|
-
4.
|
|
71
|
-
5. ABSPATH not defined yet (happens bootstrap.php line 22)
|
|
72
|
-
6. Script exits silently
|
|
73
|
-
7. PHPUnit never starts
|
|
46
|
+
1. bootstrap.php requires vendor/autoload.php
|
|
47
|
+
2. Composer loads autoload "files" immediately
|
|
48
|
+
3. helpers.php: if (!defined('ABSPATH')) { exit; }
|
|
49
|
+
4. ABSPATH not yet defined → silent exit → PHPUnit never starts
|
|
74
50
|
```
|
|
75
51
|
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"autoload": {
|
|
80
|
-
"files": [
|
|
81
|
-
"includes/helpers/url-validation.php",
|
|
82
|
-
"includes/helpers/share-urls.php"
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
```
|
|
52
|
+
Fix: Remove `autoload.files`, load helpers manually in bootstrap AFTER defining `ABSPATH`.
|
|
87
53
|
|
|
88
|
-
**✅ FIXED** (no autoload files):
|
|
89
54
|
```json
|
|
90
55
|
{
|
|
91
|
-
// NO autoload section with files array!
|
|
92
|
-
// Bootstrap loads helpers manually AFTER defining ABSPATH
|
|
93
56
|
"autoload-dev": {
|
|
94
|
-
"psr-4": {
|
|
95
|
-
"MyPlugin\\Tests\\": "tests/"
|
|
96
|
-
}
|
|
57
|
+
"psr-4": { "MyPlugin\\Tests\\": "tests/" }
|
|
97
58
|
}
|
|
98
59
|
}
|
|
99
60
|
```
|
|
@@ -102,90 +63,43 @@ Expert guidance for PHPUnit testing in WordPress plugins, emphasizing pure funct
|
|
|
102
63
|
|
|
103
64
|
## Environment Setup
|
|
104
65
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
**Important**: Composer and PHPUnit need Local WP's PHP environment. Git commands work in normal terminal.
|
|
108
|
-
|
|
109
|
-
**Use the project's configured Local WP environment**:
|
|
110
|
-
|
|
111
|
-
The `wp-local` skill provides the environment loader pattern. For composer/phpunit commands, create a similar wrapper or use the pattern directly:
|
|
66
|
+
Composer/PHPUnit require Local WP's PHP environment. Git commands work in normal terminal.
|
|
112
67
|
|
|
113
68
|
```bash
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
# 2. Runs command in that environment
|
|
69
|
+
# Run tests in Local WP environment
|
|
70
|
+
bash -c "source ~/kitty/load-localwp-env.sh $(cat .wp-local || echo $WP_LOCAL_SITE) && composer test"
|
|
117
71
|
|
|
118
|
-
#
|
|
119
|
-
bash -c "source ~/kitty/load-localwp-env.sh
|
|
120
|
-
|
|
121
|
-
# Or install dependencies:
|
|
122
|
-
bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer install"
|
|
72
|
+
# Useful aliases (~/.zshrc)
|
|
73
|
+
alias wptest='bash -c "source ~/kitty/load-localwp-env.sh $(cat .wp-local || echo $WP_LOCAL_SITE) && composer test"'
|
|
74
|
+
alias wpcomposer='bash -c "source ~/kitty/load-localwp-env.sh $(cat .wp-local || echo $WP_LOCAL_SITE) && composer $@"'
|
|
123
75
|
```
|
|
124
76
|
|
|
125
|
-
**
|
|
126
|
-
```bash
|
|
127
|
-
alias wptest='bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer test"'
|
|
128
|
-
alias wpcomposer='bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer $@"'
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Then simply:
|
|
132
|
-
```bash
|
|
133
|
-
wptest # Run tests
|
|
134
|
-
wpcomposer install # Install dependencies
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Environment Configuration
|
|
77
|
+
**Site resolution priority**: `$WP_LOCAL_SITE` env var → `.wp-local` file → error
|
|
138
78
|
|
|
139
|
-
|
|
140
|
-
1. `$WP_LOCAL_SITE` environment variable (set by Kitty terminal)
|
|
141
|
-
2. `.wp-local` file in project root (site UUID like `19efkkzWB`)
|
|
142
|
-
3. Error if neither configured
|
|
79
|
+
### Quick Diagnosis
|
|
143
80
|
|
|
144
|
-
**For git operations** (no environment needed):
|
|
145
81
|
```bash
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
git commit -m "fix: phpunit setup"
|
|
149
|
-
git push
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Quick Test Diagnosis
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
# 1. Check if bootstrap prints (should see "Bootstrap Loaded")
|
|
156
|
-
php tests/bootstrap.php
|
|
157
|
-
|
|
158
|
-
# 2. Check if PHPUnit is installed
|
|
159
|
-
ls -la vendor/bin/phpunit
|
|
160
|
-
|
|
161
|
-
# 3. Check composer.json scripts
|
|
82
|
+
php tests/bootstrap.php # Should print "Bootstrap Loaded"
|
|
83
|
+
ls -la vendor/bin/phpunit # Verify phpunit installed
|
|
162
84
|
cat composer.json | grep -A 3 scripts
|
|
163
|
-
|
|
164
|
-
# 4. Check for autoload files bug
|
|
165
85
|
cat composer.json | grep -A 5 autoload
|
|
166
86
|
```
|
|
167
87
|
|
|
168
88
|
---
|
|
169
89
|
|
|
170
|
-
##
|
|
90
|
+
## Working Templates
|
|
171
91
|
|
|
172
92
|
### composer.json
|
|
93
|
+
|
|
173
94
|
```json
|
|
174
95
|
{
|
|
175
96
|
"name": "ima-network/my-plugin",
|
|
176
|
-
"description": "Plugin description",
|
|
177
97
|
"type": "wordpress-plugin",
|
|
178
98
|
"license": "GPL-2.0-or-later",
|
|
179
|
-
"require": {
|
|
180
|
-
|
|
181
|
-
},
|
|
182
|
-
"require-dev": {
|
|
183
|
-
"phpunit/phpunit": "^9.5"
|
|
184
|
-
},
|
|
99
|
+
"require": { "php": ">=7.4" },
|
|
100
|
+
"require-dev": { "phpunit/phpunit": "^9.5" },
|
|
185
101
|
"autoload-dev": {
|
|
186
|
-
"psr-4": {
|
|
187
|
-
"MyPlugin\\Tests\\": "tests/"
|
|
188
|
-
}
|
|
102
|
+
"psr-4": { "MyPlugin\\Tests\\": "tests/" }
|
|
189
103
|
},
|
|
190
104
|
"scripts": {
|
|
191
105
|
"test": "phpunit --colors=always --testdox",
|
|
@@ -195,6 +109,7 @@ cat composer.json | grep -A 5 autoload
|
|
|
195
109
|
```
|
|
196
110
|
|
|
197
111
|
### phpunit.xml
|
|
112
|
+
|
|
198
113
|
```xml
|
|
199
114
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
200
115
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
@@ -225,236 +140,126 @@ cat composer.json | grep -A 5 autoload
|
|
|
225
140
|
```
|
|
226
141
|
|
|
227
142
|
### tests/bootstrap.php
|
|
143
|
+
|
|
228
144
|
```php
|
|
229
145
|
<?php
|
|
230
|
-
/**
|
|
231
|
-
* PHPUnit Bootstrap for My Plugin
|
|
232
|
-
*
|
|
233
|
-
* Loads pure functions for unit testing without WordPress dependencies.
|
|
234
|
-
*/
|
|
235
146
|
declare(strict_types=1);
|
|
236
147
|
|
|
237
|
-
// 1.
|
|
148
|
+
// 1. Autoloader first (no autoload.files — removed!)
|
|
238
149
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
|
239
150
|
|
|
240
|
-
// 2. Define ABSPATH
|
|
151
|
+
// 2. Define ABSPATH before loading any plugin files
|
|
241
152
|
if (!defined('ABSPATH')) {
|
|
242
153
|
define('ABSPATH', '/tmp/wordpress/');
|
|
243
154
|
}
|
|
244
155
|
|
|
245
|
-
// 3.
|
|
156
|
+
// 3. Plugin constants
|
|
246
157
|
if (!defined('MY_PLUGIN_PATH')) {
|
|
247
158
|
define('MY_PLUGIN_PATH', dirname(__DIR__) . '/');
|
|
248
159
|
}
|
|
249
160
|
|
|
250
|
-
// 4.
|
|
161
|
+
// 4. Load helpers manually (AFTER ABSPATH)
|
|
251
162
|
require_once MY_PLUGIN_PATH . 'includes/helpers/url-validation.php';
|
|
252
163
|
require_once MY_PLUGIN_PATH . 'includes/helpers/share-urls.php';
|
|
253
164
|
|
|
254
|
-
// 5.
|
|
165
|
+
// 5. Minimal WP function mocks
|
|
255
166
|
if (!function_exists('home_url')) {
|
|
256
167
|
function home_url(string $path = ''): string {
|
|
257
168
|
return 'https://example.com' . $path;
|
|
258
169
|
}
|
|
259
170
|
}
|
|
260
|
-
|
|
261
171
|
if (!function_exists('sanitize_text_field')) {
|
|
262
172
|
function sanitize_text_field(string $str): string {
|
|
263
|
-
|
|
264
|
-
$filtered = str_replace(["\r", "\n"], '', $filtered);
|
|
265
|
-
return trim($filtered);
|
|
173
|
+
return trim(str_replace(["\r", "\n"], '', strip_tags($str)));
|
|
266
174
|
}
|
|
267
175
|
}
|
|
268
|
-
|
|
269
176
|
if (!function_exists('esc_html')) {
|
|
270
177
|
function esc_html(string $text): string {
|
|
271
178
|
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
|
272
179
|
}
|
|
273
180
|
}
|
|
274
181
|
|
|
275
|
-
|
|
276
|
-
echo "✅ My Plugin Test Bootstrap Loaded\n";
|
|
182
|
+
echo "My Plugin Test Bootstrap Loaded\n";
|
|
277
183
|
```
|
|
278
184
|
|
|
279
185
|
---
|
|
280
186
|
|
|
281
|
-
## What to Test
|
|
187
|
+
## What to Test
|
|
282
188
|
|
|
283
|
-
###
|
|
189
|
+
### DO: Pure Business Logic
|
|
284
190
|
|
|
285
191
|
```php
|
|
286
192
|
<?php
|
|
287
|
-
//
|
|
288
|
-
declare(strict_types=1);
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* PURE function - Zero WordPress dependencies
|
|
292
|
-
* Perfect for unit testing
|
|
293
|
-
*/
|
|
193
|
+
// Pure function — zero WordPress dependencies, perfect for unit testing
|
|
294
194
|
function my_plugin_validate_email_pure(string $email): array {
|
|
295
|
-
if (empty($email))
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
300
|
-
return ['valid' => false, 'error' => 'Invalid email format'];
|
|
301
|
-
}
|
|
195
|
+
if (empty($email)) return ['valid' => false, 'error' => 'Email required'];
|
|
196
|
+
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return ['valid' => false, 'error' => 'Invalid email format'];
|
|
302
197
|
|
|
303
|
-
// Check for disposable domains
|
|
304
198
|
$disposable = ['tempmail.com', 'throwaway.email'];
|
|
305
199
|
$domain = substr(strrchr($email, '@'), 1);
|
|
306
|
-
if (in_array($domain, $disposable))
|
|
307
|
-
return ['valid' => false, 'error' => 'Disposable email not allowed'];
|
|
308
|
-
}
|
|
200
|
+
if (in_array($domain, $disposable)) return ['valid' => false, 'error' => 'Disposable email not allowed'];
|
|
309
201
|
|
|
310
202
|
return ['valid' => true];
|
|
311
203
|
}
|
|
312
204
|
```
|
|
313
205
|
|
|
314
|
-
**Test**:
|
|
315
206
|
```php
|
|
316
207
|
<?php
|
|
317
|
-
// tests/Unit/ValidationTest.php
|
|
318
|
-
use PHPUnit\Framework\TestCase;
|
|
319
|
-
|
|
320
208
|
class ValidationTest extends TestCase {
|
|
321
209
|
public function test_empty_email_returns_error() {
|
|
322
210
|
$result = my_plugin_validate_email_pure('');
|
|
323
|
-
|
|
324
211
|
$this->assertFalse($result['valid']);
|
|
325
212
|
$this->assertEquals('Email required', $result['error']);
|
|
326
213
|
}
|
|
327
214
|
|
|
328
|
-
public function test_invalid_format_returns_error() {
|
|
329
|
-
$result = my_plugin_validate_email_pure('not-an-email');
|
|
330
|
-
|
|
331
|
-
$this->assertFalse($result['valid']);
|
|
332
|
-
$this->assertEquals('Invalid email format', $result['error']);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
public function test_disposable_email_rejected() {
|
|
336
|
-
$result = my_plugin_validate_email_pure('user@tempmail.com');
|
|
337
|
-
|
|
338
|
-
$this->assertFalse($result['valid']);
|
|
339
|
-
$this->assertEquals('Disposable email not allowed', $result['error']);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
215
|
public function test_valid_email_passes() {
|
|
343
216
|
$result = my_plugin_validate_email_pure('user@example.com');
|
|
344
|
-
|
|
345
217
|
$this->assertTrue($result['valid']);
|
|
346
|
-
$this->assertArrayNotHasKey('error', $result);
|
|
347
218
|
}
|
|
348
219
|
|
|
349
|
-
// FP Principle: Test determinism
|
|
350
220
|
public function test_function_is_deterministic() {
|
|
351
|
-
$
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
$this->assertEquals($result1, $result2);
|
|
221
|
+
$this->assertEquals(
|
|
222
|
+
my_plugin_validate_email_pure('test@example.com'),
|
|
223
|
+
my_plugin_validate_email_pure('test@example.com')
|
|
224
|
+
);
|
|
356
225
|
}
|
|
357
226
|
}
|
|
358
227
|
```
|
|
359
228
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
### ❌ DON'T TEST: WordPress Integration Wrappers
|
|
229
|
+
### DON'T: WordPress Integration Wrappers
|
|
363
230
|
|
|
364
231
|
```php
|
|
365
232
|
<?php
|
|
366
|
-
//
|
|
233
|
+
// Skip unit testing this — it's 100% WordPress integration
|
|
367
234
|
function my_plugin_ajax_validate_email() {
|
|
368
|
-
// DON'T unit test this - it's all WordPress integration!
|
|
369
235
|
check_ajax_referer('validate_email_nonce', 'nonce');
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
wp_send_json_error('Unauthorized', 403);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
$email = sanitize_email($_POST['email']);
|
|
376
|
-
|
|
377
|
-
// Call pure function (which IS tested)
|
|
378
|
-
$result = my_plugin_validate_email_pure($email);
|
|
379
|
-
|
|
380
|
-
wp_send_json_success($result);
|
|
236
|
+
if (!current_user_can('read')) wp_send_json_error('Unauthorized', 403);
|
|
237
|
+
wp_send_json_success(my_plugin_validate_email_pure(sanitize_email($_POST['email'])));
|
|
381
238
|
}
|
|
382
|
-
add_action('wp_ajax_my_plugin_validate_email', 'my_plugin_ajax_validate_email');
|
|
383
239
|
```
|
|
384
240
|
|
|
385
|
-
|
|
386
|
-
- Full WordPress environment
|
|
387
|
-
- Mocking `$_POST`, nonces, capabilities, AJAX functions
|
|
388
|
-
- Complex setup that's brittle and slow
|
|
389
|
-
|
|
390
|
-
**Better approach**:
|
|
391
|
-
- Unit test the pure function (`my_plugin_validate_email_pure`) ✅
|
|
392
|
-
- Integration test the AJAX handler with real WordPress (separate test suite)
|
|
241
|
+
Unit test the pure function. Integration test the AJAX handler with a real WP environment.
|
|
393
242
|
|
|
394
243
|
---
|
|
395
244
|
|
|
396
|
-
## Mocking Rules
|
|
397
|
-
|
|
398
|
-
### Mock Only What's Needed for Pure Context
|
|
399
|
-
|
|
400
|
-
**❌ DON'T over-mock**:
|
|
401
|
-
```php
|
|
402
|
-
<?php
|
|
403
|
-
// Excessive mocking for a pure function test
|
|
404
|
-
if (!function_exists('wp_remote_post')) { /* ... */ }
|
|
405
|
-
if (!function_exists('wp_remote_get')) { /* ... */ }
|
|
406
|
-
if (!function_exists('get_option')) { /* ... */ }
|
|
407
|
-
if (!function_exists('update_option')) { /* ... */ }
|
|
408
|
-
if (!function_exists('delete_option')) { /* ... */ }
|
|
409
|
-
// ... 50 more mocks you don't need
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
**✅ DO mock minimally**:
|
|
413
|
-
```php
|
|
414
|
-
<?php
|
|
415
|
-
// Only mock what pure functions actually use
|
|
416
|
-
if (!function_exists('sanitize_text_field')) {
|
|
417
|
-
function sanitize_text_field(string $str): string {
|
|
418
|
-
return trim(strip_tags($str));
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!function_exists('home_url')) {
|
|
423
|
-
function home_url(string $path = ''): string {
|
|
424
|
-
return 'https://example.com' . $path;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
```
|
|
245
|
+
## Mocking Rules
|
|
428
246
|
|
|
429
|
-
|
|
247
|
+
Mock only what pure functions actually call. Common mocks for pure functions:
|
|
430
248
|
|
|
431
249
|
```php
|
|
432
250
|
<?php
|
|
433
|
-
// WordPress utility functions (pure-ish)
|
|
434
251
|
if (!function_exists('wp_parse_args')) {
|
|
435
252
|
function wp_parse_args($args, $defaults = []): array {
|
|
436
|
-
if (is_string($args)) {
|
|
437
|
-
parse_str($args, $parsed_args);
|
|
438
|
-
$args = $parsed_args;
|
|
439
|
-
}
|
|
253
|
+
if (is_string($args)) { parse_str($args, $args); }
|
|
440
254
|
return array_merge($defaults, (array) $args);
|
|
441
255
|
}
|
|
442
256
|
}
|
|
443
|
-
|
|
444
|
-
// Sanitization functions
|
|
445
257
|
if (!function_exists('sanitize_email')) {
|
|
446
|
-
function sanitize_email(string $email): string {
|
|
447
|
-
return strtolower(trim($email));
|
|
448
|
-
}
|
|
258
|
+
function sanitize_email(string $email): string { return strtolower(trim($email)); }
|
|
449
259
|
}
|
|
450
|
-
|
|
451
260
|
if (!function_exists('esc_url_raw')) {
|
|
452
|
-
function esc_url_raw(string $url): string {
|
|
453
|
-
return filter_var($url, FILTER_SANITIZE_URL) ?: '';
|
|
454
|
-
}
|
|
261
|
+
function esc_url_raw(string $url): string { return filter_var($url, FILTER_SANITIZE_URL) ?: ''; }
|
|
455
262
|
}
|
|
456
|
-
|
|
457
|
-
// URL parsing (WordPress wrapper for parse_url)
|
|
458
263
|
if (!function_exists('wp_parse_url')) {
|
|
459
264
|
function wp_parse_url(string $url, int $component = -1) {
|
|
460
265
|
return $component === -1 ? parse_url($url) : parse_url($url, $component);
|
|
@@ -464,241 +269,82 @@ if (!function_exists('wp_parse_url')) {
|
|
|
464
269
|
|
|
465
270
|
---
|
|
466
271
|
|
|
467
|
-
## Test Organization
|
|
468
|
-
|
|
469
|
-
### Pattern 1: Pure Function Testing (Fastest)
|
|
470
|
-
|
|
471
|
-
```
|
|
472
|
-
tests/
|
|
473
|
-
└── Unit/
|
|
474
|
-
├── ValidationTest.php # Pure validation functions
|
|
475
|
-
├── CalculationTest.php # Pure calculation logic
|
|
476
|
-
└── FormatterTest.php # Pure formatting functions
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
**Characteristics**:
|
|
480
|
-
- No WordPress dependencies
|
|
481
|
-
- Fast (<100ms total)
|
|
482
|
-
- No database, no HTTP, no filesystem
|
|
483
|
-
- Run on every commit
|
|
484
|
-
|
|
485
|
-
### Pattern 2: Integration Testing (Slower)
|
|
272
|
+
## Test Organization
|
|
486
273
|
|
|
487
274
|
```
|
|
488
275
|
tests/
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
276
|
+
├── Unit/ # Pure functions — no WP, <100ms, run every commit
|
|
277
|
+
│ ├── ValidationTest.php
|
|
278
|
+
│ ├── CalculationTest.php
|
|
279
|
+
│ └── FormatterTest.php
|
|
280
|
+
└── Integration/ # Full WP env — run before releases
|
|
281
|
+
├── AjaxHandlerTest.php
|
|
282
|
+
└── DatabaseTest.php
|
|
493
283
|
```
|
|
494
284
|
|
|
495
|
-
**Characteristics**:
|
|
496
|
-
- Requires full WordPress installation
|
|
497
|
-
- Uses WP_UnitTestCase from wordpress-develop
|
|
498
|
-
- Slower (seconds to minutes)
|
|
499
|
-
- Run before releases
|
|
500
|
-
|
|
501
285
|
---
|
|
502
286
|
|
|
503
|
-
##
|
|
287
|
+
## Patterns by Function Type
|
|
504
288
|
|
|
505
|
-
### Pure Calculations
|
|
506
289
|
```php
|
|
507
|
-
|
|
508
|
-
function calculate_discount_pure(float $price, float $rate): float {
|
|
509
|
-
return round($price * (1 - $rate), 2);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Test: Assert exact values
|
|
290
|
+
// Calculations — assert exact values
|
|
513
291
|
$this->assertEquals(90.0, calculate_discount_pure(100.0, 0.10));
|
|
514
|
-
$this->assertEquals(95.0, calculate_discount_pure(100.0, 0.05));
|
|
515
|
-
```
|
|
516
292
|
|
|
517
|
-
|
|
518
|
-
```php
|
|
519
|
-
<?php
|
|
520
|
-
function validate_age_pure(int $age): array {
|
|
521
|
-
if ($age < 0) return ['valid' => false, 'error' => 'Negative age'];
|
|
522
|
-
if ($age < 18) return ['valid' => false, 'error' => 'Must be 18+'];
|
|
523
|
-
return ['valid' => true];
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Test: Assert result structure
|
|
293
|
+
// Validation — assert result structure
|
|
527
294
|
$result = validate_age_pure(15);
|
|
528
295
|
$this->assertFalse($result['valid']);
|
|
529
296
|
$this->assertArrayHasKey('error', $result);
|
|
530
|
-
```
|
|
531
297
|
|
|
532
|
-
|
|
533
|
-
```php
|
|
534
|
-
<?php
|
|
535
|
-
function format_phone_pure(string $phone): string {
|
|
536
|
-
$digits = preg_replace('/\D/', '', $phone);
|
|
537
|
-
if (strlen($digits) === 10) {
|
|
538
|
-
return sprintf('(%s) %s-%s',
|
|
539
|
-
substr($digits, 0, 3),
|
|
540
|
-
substr($digits, 3, 3),
|
|
541
|
-
substr($digits, 6, 4)
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
return $phone;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Test: Assert transformations
|
|
298
|
+
// Transformations — assert output shape
|
|
548
299
|
$this->assertEquals('(555) 123-4567', format_phone_pure('5551234567'));
|
|
549
300
|
$this->assertEquals('(555) 123-4567', format_phone_pure('555-123-4567'));
|
|
550
|
-
$this->assertEquals('invalid', format_phone_pure('invalid'));
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
---
|
|
554
301
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
```php
|
|
560
|
-
<?php
|
|
561
|
-
public function test_performance_fast_execution() {
|
|
562
|
-
$iterations = 10000;
|
|
563
|
-
$start = microtime(true);
|
|
564
|
-
|
|
565
|
-
for ($i = 0; $i < $iterations; $i++) {
|
|
566
|
-
my_plugin_validate_email_pure('user@example.com');
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
$elapsed = microtime(true) - $start;
|
|
570
|
-
|
|
571
|
-
// Should complete 10k validations in < 100ms
|
|
572
|
-
$this->assertLessThan(0.1, $elapsed);
|
|
573
|
-
}
|
|
302
|
+
// Performance — pure functions enable this for free
|
|
303
|
+
$start = microtime(true);
|
|
304
|
+
for ($i = 0; $i < 10000; $i++) { my_plugin_validate_email_pure('user@example.com'); }
|
|
305
|
+
$this->assertLessThan(0.1, microtime(true) - $start);
|
|
574
306
|
```
|
|
575
307
|
|
|
576
308
|
---
|
|
577
309
|
|
|
578
|
-
##
|
|
579
|
-
|
|
580
|
-
### ❌ Testing Private Methods
|
|
310
|
+
## Anti-Patterns
|
|
581
311
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
$reflection = new ReflectionClass(MyClass::class);
|
|
588
|
-
$method = $reflection->getMethod('privateMethod');
|
|
589
|
-
$method->setAccessible(true);
|
|
590
|
-
// ...
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// DO extract pure function and test that
|
|
595
|
-
function my_plugin_process_data_pure(array $data): array {
|
|
596
|
-
// Extracted logic, now testable
|
|
597
|
-
return $data;
|
|
598
|
-
}
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
### ❌ Testing Implementation Details
|
|
602
|
-
|
|
603
|
-
```php
|
|
604
|
-
<?php
|
|
605
|
-
// DON'T test internal variable values
|
|
606
|
-
public function test_internal_state() {
|
|
607
|
-
$obj = new MyClass();
|
|
608
|
-
$this->assertEquals(5, $obj->internalCounter); // Brittle!
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// DO test public behavior
|
|
612
|
-
public function test_public_behavior() {
|
|
613
|
-
$result = my_plugin_process_items(['a', 'b', 'c']);
|
|
614
|
-
$this->assertCount(3, $result);
|
|
615
|
-
}
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### ❌ Over-Mocking
|
|
619
|
-
|
|
620
|
-
```php
|
|
621
|
-
<?php
|
|
622
|
-
// DON'T mock everything
|
|
623
|
-
$mock = $this->createMock(Database::class);
|
|
624
|
-
$mock->method('query')->willReturn([]);
|
|
625
|
-
$mock->method('insert')->willReturn(1);
|
|
626
|
-
$mock->method('update')->willReturn(true);
|
|
627
|
-
// ... 20 more mocks
|
|
628
|
-
|
|
629
|
-
// DO test pure functions that don't need mocks
|
|
630
|
-
function process_results_pure(array $results): array {
|
|
631
|
-
return array_map('strtoupper', $results);
|
|
632
|
-
}
|
|
633
|
-
```
|
|
312
|
+
| Avoid | Do Instead |
|
|
313
|
+
|-------|-----------|
|
|
314
|
+
| Reflection to test private methods | Extract as testable pure function |
|
|
315
|
+
| Test internal variable values | Test public behavior/output |
|
|
316
|
+
| Mock 20+ WordPress functions | Redesign so pure function needs <5 mocks |
|
|
634
317
|
|
|
635
318
|
---
|
|
636
319
|
|
|
637
320
|
## Quality Gates
|
|
638
321
|
|
|
639
|
-
|
|
640
|
-
- [ ]
|
|
641
|
-
- [ ]
|
|
642
|
-
- [ ]
|
|
643
|
-
- [ ]
|
|
644
|
-
- [ ]
|
|
645
|
-
- [ ]
|
|
646
|
-
- [ ] Test assertions are deterministic (no flaky tests)
|
|
322
|
+
- [ ] `composer test` produces visible output
|
|
323
|
+
- [ ] Bootstrap prints confirmation message
|
|
324
|
+
- [ ] Pure function tests run in <100ms
|
|
325
|
+
- [ ] No `autoload.files` in composer.json
|
|
326
|
+
- [ ] Pure business logic separated from WP wrappers
|
|
327
|
+
- [ ] Mock count <10 functions
|
|
328
|
+
- [ ] No flaky assertions
|
|
647
329
|
|
|
648
330
|
---
|
|
649
331
|
|
|
650
|
-
##
|
|
332
|
+
## Reference Plugins
|
|
651
333
|
|
|
652
|
-
**Reference plugins with working tests**:
|
|
653
334
|
```bash
|
|
654
|
-
|
|
655
|
-
cd wp-content/plugins/ima-
|
|
656
|
-
composer test
|
|
657
|
-
|
|
658
|
-
# ima-shortcodes: Recently fixed
|
|
659
|
-
cd wp-content/plugins/ima-shortcodes
|
|
660
|
-
composer test
|
|
335
|
+
cd wp-content/plugins/ima-forms && composer test # Gold standard
|
|
336
|
+
cd wp-content/plugins/ima-shortcodes && composer test # Recently fixed
|
|
661
337
|
```
|
|
662
338
|
|
|
663
339
|
---
|
|
664
340
|
|
|
665
341
|
## Troubleshooting
|
|
666
342
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
### Issue: Tests hang/exit silently
|
|
674
|
-
|
|
675
|
-
**Symptom**: Process hangs or exits without error
|
|
676
|
-
|
|
677
|
-
**Diagnosis**:
|
|
678
|
-
```bash
|
|
679
|
-
# Check if bootstrap runs
|
|
680
|
-
php tests/bootstrap.php
|
|
681
|
-
|
|
682
|
-
# Check for autoload files bug
|
|
683
|
-
cat composer.json | grep -A 5 autoload
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
**Fix**: Remove autoload files section, load manually in bootstrap
|
|
687
|
-
|
|
688
|
-
### Issue: "Class not found" errors
|
|
689
|
-
|
|
690
|
-
**Symptom**: PHPUnit can't find test classes
|
|
691
|
-
|
|
692
|
-
**Fix**: Check `autoload-dev` PSR-4 mapping in composer.json:
|
|
693
|
-
```json
|
|
694
|
-
"autoload-dev": {
|
|
695
|
-
"psr-4": {
|
|
696
|
-
"MyPlugin\\Tests\\": "tests/"
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
Then run: `composer dump-autoload`
|
|
343
|
+
| Symptom | Cause | Fix |
|
|
344
|
+
|---------|-------|-----|
|
|
345
|
+
| No output from `composer test` | Missing `--testdox` | Add `--testdox` to scripts |
|
|
346
|
+
| Hang/silent exit | `autoload.files` loads before ABSPATH | Remove files array, load manually in bootstrap |
|
|
347
|
+
| "Class not found" | Bad PSR-4 mapping | Check `autoload-dev` in composer.json, run `composer dump-autoload` |
|
|
702
348
|
|
|
703
349
|
---
|
|
704
350
|
|
|
@@ -706,11 +352,5 @@ Then run: `composer dump-autoload`
|
|
|
706
352
|
|
|
707
353
|
- `../php-fp/SKILL.md` - Core FP principles
|
|
708
354
|
- `../php-fp-wordpress/SKILL.md` - WordPress security + FP
|
|
709
|
-
- `../wp-local/SKILL.md` - Local WP environment
|
|
710
|
-
-
|
|
711
|
-
- WordPress test suite: https://make.wordpress.org/core/handbook/testing/automated-testing/phpunit/
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
**Last Updated**: 2026-01-29
|
|
716
|
-
**Discovery**: After debugging the same bugs across multiple plugins (ima-forms, ima-shortcodes, ima-access-control)
|
|
355
|
+
- `../wp-local/SKILL.md` - Local WP environment
|
|
356
|
+
- https://phpunit.de/documentation.html
|