ima-claude 2.9.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/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "phpunit-wp"
|
|
3
|
+
description: "PHPUnit testing for WordPress plugins with FP principles - fast unit tests, minimal mocks, environment-aware setup. Triggers on: phpunit, unit test, test wordpress, composer test, test bootstrap, mock wordpress."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PHPUnit for WordPress Plugins
|
|
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
|
+
## Core Philosophy
|
|
19
|
+
|
|
20
|
+
**Test pure functions, not WordPress integration.**
|
|
21
|
+
|
|
22
|
+
1. **Pure business logic** = Fast unit tests (<100ms, no WordPress)
|
|
23
|
+
2. **WordPress wrappers** = Integration tests (slower, full WP environment)
|
|
24
|
+
3. **Mock minimally** = Only mock what's needed for pure function context
|
|
25
|
+
|
|
26
|
+
**Foundation**: Reference `../php-fp/SKILL.md` and `../php-fp-wordpress/SKILL.md` for FP patterns.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## THE TWO CRITICAL SETUP BUGS (We Keep Forgetting!)
|
|
31
|
+
|
|
32
|
+
### 🐛 Bug #1: Silent PHPUnit Execution
|
|
33
|
+
|
|
34
|
+
**Symptom**: `composer test` produces ZERO output, not even "no tests found"
|
|
35
|
+
|
|
36
|
+
**Root Cause**: PHPUnit 9.x doesn't output anything without `--testdox` flag
|
|
37
|
+
|
|
38
|
+
**❌ BROKEN** (silent execution):
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "phpunit"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**✅ FIXED** (visible output):
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"scripts": {
|
|
51
|
+
"test": "phpunit --colors=always --testdox",
|
|
52
|
+
"test:coverage": "phpunit --coverage-html coverage"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### 🐛 Bug #2: Autoload Files Kill Tests
|
|
60
|
+
|
|
61
|
+
**Symptom**: Tests hang or exit silently with no error message
|
|
62
|
+
|
|
63
|
+
**Root Cause**: Composer autoload runs BEFORE test bootstrap defines `ABSPATH`
|
|
64
|
+
|
|
65
|
+
**The Fatal Flow**:
|
|
66
|
+
```
|
|
67
|
+
1. bootstrap.php line 17: require vendor/autoload.php
|
|
68
|
+
2. Composer sees: "autoload": { "files": ["includes/helpers.php"] }
|
|
69
|
+
3. Composer loads helpers.php IMMEDIATELY
|
|
70
|
+
4. helpers.php line 14: if (!defined('ABSPATH')) { exit; }
|
|
71
|
+
5. ABSPATH not defined yet (happens bootstrap.php line 22)
|
|
72
|
+
6. Script exits silently
|
|
73
|
+
7. PHPUnit never starts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**❌ BROKEN** (causes silent exit):
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"autoload": {
|
|
80
|
+
"files": [
|
|
81
|
+
"includes/helpers/url-validation.php",
|
|
82
|
+
"includes/helpers/share-urls.php"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**✅ FIXED** (no autoload files):
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
// NO autoload section with files array!
|
|
92
|
+
// Bootstrap loads helpers manually AFTER defining ABSPATH
|
|
93
|
+
"autoload-dev": {
|
|
94
|
+
"psr-4": {
|
|
95
|
+
"MyPlugin\\Tests\\": "tests/"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Environment Setup
|
|
104
|
+
|
|
105
|
+
### Running Tests in Local WP Environment
|
|
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:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Pattern from wp-local skill:
|
|
115
|
+
# 1. Sources ~/kitty/load-localwp-env.sh with site name from $WP_LOCAL_SITE or .wp-local
|
|
116
|
+
# 2. Runs command in that environment
|
|
117
|
+
|
|
118
|
+
# Example - Run tests:
|
|
119
|
+
bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer test"
|
|
120
|
+
|
|
121
|
+
# Or install dependencies:
|
|
122
|
+
bash -c "source ~/kitty/load-localwp-env.sh \$(cat .wp-local || echo \$WP_LOCAL_SITE) && composer install"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Pro Tip**: Create a shell alias in your `~/.bashrc` or `~/.zshrc`:
|
|
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
|
|
138
|
+
|
|
139
|
+
**Priority order** (same as wp-local):
|
|
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
|
|
143
|
+
|
|
144
|
+
**For git operations** (no environment needed):
|
|
145
|
+
```bash
|
|
146
|
+
# Regular terminal works fine - git doesn't need PHP environment
|
|
147
|
+
git status
|
|
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
|
|
162
|
+
cat composer.json | grep -A 3 scripts
|
|
163
|
+
|
|
164
|
+
# 4. Check for autoload files bug
|
|
165
|
+
cat composer.json | grep -A 5 autoload
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Copy-Paste Working Template
|
|
171
|
+
|
|
172
|
+
### composer.json
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"name": "ima-network/my-plugin",
|
|
176
|
+
"description": "Plugin description",
|
|
177
|
+
"type": "wordpress-plugin",
|
|
178
|
+
"license": "GPL-2.0-or-later",
|
|
179
|
+
"require": {
|
|
180
|
+
"php": ">=7.4"
|
|
181
|
+
},
|
|
182
|
+
"require-dev": {
|
|
183
|
+
"phpunit/phpunit": "^9.5"
|
|
184
|
+
},
|
|
185
|
+
"autoload-dev": {
|
|
186
|
+
"psr-4": {
|
|
187
|
+
"MyPlugin\\Tests\\": "tests/"
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"scripts": {
|
|
191
|
+
"test": "phpunit --colors=always --testdox",
|
|
192
|
+
"test:coverage": "phpunit --coverage-html coverage"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### phpunit.xml
|
|
198
|
+
```xml
|
|
199
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
200
|
+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
201
|
+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
|
|
202
|
+
bootstrap="tests/bootstrap.php"
|
|
203
|
+
colors="true"
|
|
204
|
+
verbose="true"
|
|
205
|
+
stopOnFailure="false">
|
|
206
|
+
<testsuites>
|
|
207
|
+
<testsuite name="Unit">
|
|
208
|
+
<directory>tests/Unit</directory>
|
|
209
|
+
</testsuite>
|
|
210
|
+
</testsuites>
|
|
211
|
+
<coverage>
|
|
212
|
+
<include>
|
|
213
|
+
<directory suffix=".php">includes</directory>
|
|
214
|
+
</include>
|
|
215
|
+
<exclude>
|
|
216
|
+
<directory>tests</directory>
|
|
217
|
+
<directory>templates</directory>
|
|
218
|
+
</exclude>
|
|
219
|
+
</coverage>
|
|
220
|
+
<php>
|
|
221
|
+
<ini name="error_reporting" value="E_ALL"/>
|
|
222
|
+
<ini name="display_errors" value="true"/>
|
|
223
|
+
</php>
|
|
224
|
+
</phpunit>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### tests/bootstrap.php
|
|
228
|
+
```php
|
|
229
|
+
<?php
|
|
230
|
+
/**
|
|
231
|
+
* PHPUnit Bootstrap for My Plugin
|
|
232
|
+
*
|
|
233
|
+
* Loads pure functions for unit testing without WordPress dependencies.
|
|
234
|
+
*/
|
|
235
|
+
declare(strict_types=1);
|
|
236
|
+
|
|
237
|
+
// 1. Load Composer autoloader FIRST (but it won't autoload files - we removed that!)
|
|
238
|
+
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
|
239
|
+
|
|
240
|
+
// 2. Define ABSPATH to prevent plugin file exits
|
|
241
|
+
if (!defined('ABSPATH')) {
|
|
242
|
+
define('ABSPATH', '/tmp/wordpress/');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3. Define plugin constants
|
|
246
|
+
if (!defined('MY_PLUGIN_PATH')) {
|
|
247
|
+
define('MY_PLUGIN_PATH', dirname(__DIR__) . '/');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 4. NOW manually load helper files (AFTER ABSPATH defined!)
|
|
251
|
+
require_once MY_PLUGIN_PATH . 'includes/helpers/url-validation.php';
|
|
252
|
+
require_once MY_PLUGIN_PATH . 'includes/helpers/share-urls.php';
|
|
253
|
+
|
|
254
|
+
// 5. Mock WordPress functions minimally
|
|
255
|
+
if (!function_exists('home_url')) {
|
|
256
|
+
function home_url(string $path = ''): string {
|
|
257
|
+
return 'https://example.com' . $path;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!function_exists('sanitize_text_field')) {
|
|
262
|
+
function sanitize_text_field(string $str): string {
|
|
263
|
+
$filtered = strip_tags($str);
|
|
264
|
+
$filtered = str_replace(["\r", "\n"], '', $filtered);
|
|
265
|
+
return trim($filtered);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!function_exists('esc_html')) {
|
|
270
|
+
function esc_html(string $text): string {
|
|
271
|
+
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 6. Confirmation message (proves bootstrap loaded)
|
|
276
|
+
echo "✅ My Plugin Test Bootstrap Loaded\n";
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## What to Test (FP Principles)
|
|
282
|
+
|
|
283
|
+
### ✅ DO TEST: Pure Business Logic
|
|
284
|
+
|
|
285
|
+
```php
|
|
286
|
+
<?php
|
|
287
|
+
// includes/helpers/validation.php
|
|
288
|
+
declare(strict_types=1);
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* PURE function - Zero WordPress dependencies
|
|
292
|
+
* Perfect for unit testing
|
|
293
|
+
*/
|
|
294
|
+
function my_plugin_validate_email_pure(string $email): array {
|
|
295
|
+
if (empty($email)) {
|
|
296
|
+
return ['valid' => false, 'error' => 'Email required'];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
300
|
+
return ['valid' => false, 'error' => 'Invalid email format'];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for disposable domains
|
|
304
|
+
$disposable = ['tempmail.com', 'throwaway.email'];
|
|
305
|
+
$domain = substr(strrchr($email, '@'), 1);
|
|
306
|
+
if (in_array($domain, $disposable)) {
|
|
307
|
+
return ['valid' => false, 'error' => 'Disposable email not allowed'];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return ['valid' => true];
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Test**:
|
|
315
|
+
```php
|
|
316
|
+
<?php
|
|
317
|
+
// tests/Unit/ValidationTest.php
|
|
318
|
+
use PHPUnit\Framework\TestCase;
|
|
319
|
+
|
|
320
|
+
class ValidationTest extends TestCase {
|
|
321
|
+
public function test_empty_email_returns_error() {
|
|
322
|
+
$result = my_plugin_validate_email_pure('');
|
|
323
|
+
|
|
324
|
+
$this->assertFalse($result['valid']);
|
|
325
|
+
$this->assertEquals('Email required', $result['error']);
|
|
326
|
+
}
|
|
327
|
+
|
|
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
|
+
public function test_valid_email_passes() {
|
|
343
|
+
$result = my_plugin_validate_email_pure('user@example.com');
|
|
344
|
+
|
|
345
|
+
$this->assertTrue($result['valid']);
|
|
346
|
+
$this->assertArrayNotHasKey('error', $result);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// FP Principle: Test determinism
|
|
350
|
+
public function test_function_is_deterministic() {
|
|
351
|
+
$email = 'test@example.com';
|
|
352
|
+
$result1 = my_plugin_validate_email_pure($email);
|
|
353
|
+
$result2 = my_plugin_validate_email_pure($email);
|
|
354
|
+
|
|
355
|
+
$this->assertEquals($result1, $result2);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### ❌ DON'T TEST: WordPress Integration Wrappers
|
|
363
|
+
|
|
364
|
+
```php
|
|
365
|
+
<?php
|
|
366
|
+
// includes/ajax-handlers.php - WordPress wrapper
|
|
367
|
+
function my_plugin_ajax_validate_email() {
|
|
368
|
+
// DON'T unit test this - it's all WordPress integration!
|
|
369
|
+
check_ajax_referer('validate_email_nonce', 'nonce');
|
|
370
|
+
|
|
371
|
+
if (!current_user_can('read')) {
|
|
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);
|
|
381
|
+
}
|
|
382
|
+
add_action('wp_ajax_my_plugin_validate_email', 'my_plugin_ajax_validate_email');
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Why not test**: This is 100% WordPress integration. Testing it requires:
|
|
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)
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Mocking Rules (Minimal Philosophy)
|
|
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
|
+
```
|
|
428
|
+
|
|
429
|
+
### Common WordPress Mocks for Pure Functions
|
|
430
|
+
|
|
431
|
+
```php
|
|
432
|
+
<?php
|
|
433
|
+
// WordPress utility functions (pure-ish)
|
|
434
|
+
if (!function_exists('wp_parse_args')) {
|
|
435
|
+
function wp_parse_args($args, $defaults = []): array {
|
|
436
|
+
if (is_string($args)) {
|
|
437
|
+
parse_str($args, $parsed_args);
|
|
438
|
+
$args = $parsed_args;
|
|
439
|
+
}
|
|
440
|
+
return array_merge($defaults, (array) $args);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Sanitization functions
|
|
445
|
+
if (!function_exists('sanitize_email')) {
|
|
446
|
+
function sanitize_email(string $email): string {
|
|
447
|
+
return strtolower(trim($email));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!function_exists('esc_url_raw')) {
|
|
452
|
+
function esc_url_raw(string $url): string {
|
|
453
|
+
return filter_var($url, FILTER_SANITIZE_URL) ?: '';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// URL parsing (WordPress wrapper for parse_url)
|
|
458
|
+
if (!function_exists('wp_parse_url')) {
|
|
459
|
+
function wp_parse_url(string $url, int $component = -1) {
|
|
460
|
+
return $component === -1 ? parse_url($url) : parse_url($url, $component);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Test Organization Patterns
|
|
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)
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
tests/
|
|
489
|
+
└── Integration/
|
|
490
|
+
├── AjaxHandlerTest.php # Full WordPress + AJAX
|
|
491
|
+
├── ShortcodeTest.php # Full WordPress + rendering
|
|
492
|
+
└── DatabaseTest.php # Full WordPress + DB
|
|
493
|
+
```
|
|
494
|
+
|
|
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
|
+
---
|
|
502
|
+
|
|
503
|
+
## Testing Patterns by Function Type
|
|
504
|
+
|
|
505
|
+
### Pure Calculations
|
|
506
|
+
```php
|
|
507
|
+
<?php
|
|
508
|
+
function calculate_discount_pure(float $price, float $rate): float {
|
|
509
|
+
return round($price * (1 - $rate), 2);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Test: Assert exact values
|
|
513
|
+
$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
|
+
|
|
517
|
+
### Pure Validation
|
|
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
|
|
527
|
+
$result = validate_age_pure(15);
|
|
528
|
+
$this->assertFalse($result['valid']);
|
|
529
|
+
$this->assertArrayHasKey('error', $result);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Pure Transformations
|
|
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
|
|
548
|
+
$this->assertEquals('(555) 123-4567', format_phone_pure('5551234567'));
|
|
549
|
+
$this->assertEquals('(555) 123-4567', format_phone_pure('555-123-4567'));
|
|
550
|
+
$this->assertEquals('invalid', format_phone_pure('invalid'));
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Performance Testing (FP Principle)
|
|
556
|
+
|
|
557
|
+
Pure functions enable easy performance testing:
|
|
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
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Common Anti-Patterns
|
|
579
|
+
|
|
580
|
+
### ❌ Testing Private Methods
|
|
581
|
+
|
|
582
|
+
```php
|
|
583
|
+
<?php
|
|
584
|
+
// DON'T use reflection to test private methods
|
|
585
|
+
class MyTest extends TestCase {
|
|
586
|
+
public function test_private_method() {
|
|
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
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Quality Gates
|
|
638
|
+
|
|
639
|
+
Before merging:
|
|
640
|
+
- [ ] `composer test` produces visible output (not silent)
|
|
641
|
+
- [ ] Bootstrap prints "Bootstrap Loaded" message
|
|
642
|
+
- [ ] Tests run in < 100ms for pure functions
|
|
643
|
+
- [ ] No autoload files section in composer.json
|
|
644
|
+
- [ ] Pure business logic separated from WordPress wrappers
|
|
645
|
+
- [ ] Mock count is minimal (< 10 functions)
|
|
646
|
+
- [ ] Test assertions are deterministic (no flaky tests)
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Working Examples
|
|
651
|
+
|
|
652
|
+
**Reference plugins with working tests**:
|
|
653
|
+
```bash
|
|
654
|
+
# ima-forms: Gold standard
|
|
655
|
+
cd wp-content/plugins/ima-forms
|
|
656
|
+
composer test
|
|
657
|
+
|
|
658
|
+
# ima-shortcodes: Recently fixed
|
|
659
|
+
cd wp-content/plugins/ima-shortcodes
|
|
660
|
+
composer test
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Troubleshooting
|
|
666
|
+
|
|
667
|
+
### Issue: Silent test execution
|
|
668
|
+
|
|
669
|
+
**Symptom**: `composer test` produces no output
|
|
670
|
+
|
|
671
|
+
**Fix**: Add `--testdox` flag to composer.json scripts
|
|
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`
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## References
|
|
706
|
+
|
|
707
|
+
- `../php-fp/SKILL.md` - Core FP principles
|
|
708
|
+
- `../php-fp-wordpress/SKILL.md` - WordPress security + FP
|
|
709
|
+
- `../wp-local/SKILL.md` - Local WP environment handling
|
|
710
|
+
- PHPUnit docs: https://phpunit.de/documentation.html
|
|
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)
|