wordpress-agent-kit 0.3.0 → 0.4.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/.github/skills/blueprint/SKILL.md +418 -0
- package/.github/skills/wp-abilities-api/SKILL.md +12 -0
- package/.github/skills/wp-abilities-api/references/delegate-helper-pattern.md +241 -0
- package/.github/skills/wp-abilities-api/references/domain-vs-projection.md +113 -0
- package/.github/skills/wp-abilities-api/references/error-code-vocabulary.md +123 -0
- package/.github/skills/wp-abilities-api/references/grouping-heuristic.md +89 -0
- package/.github/skills/wp-abilities-api/references/input-schema-gotchas.md +265 -0
- package/.github/skills/wp-abilities-api/references/php-registration.md +47 -20
- package/.github/skills/wp-abilities-api/references/plugin-family-patterns.md +233 -0
- package/.github/skills/wp-abilities-api/references/shared-core-service.md +184 -0
- package/.github/skills/wp-abilities-audit/SKILL.md +199 -0
- package/.github/skills/wp-abilities-audit/references/audit-schema.md +300 -0
- package/.github/skills/wp-abilities-audit/references/capability-gate-tracing.md +197 -0
- package/.github/skills/wp-abilities-audit/references/controller-enumeration.md +116 -0
- package/.github/skills/wp-abilities-verify/SKILL.md +215 -0
- package/.github/skills/wp-abilities-verify/references/annotation-correctness.md +154 -0
- package/.github/skills/wp-abilities-verify/references/audit-schema-validation.md +131 -0
- package/.github/skills/wp-abilities-verify/references/permission-roundtrip.md +190 -0
- package/.github/skills/wp-abilities-verify/references/runtime-harness.md +462 -0
- package/.github/skills/wp-abilities-verify/references/schema-lints.md +118 -0
- package/.github/skills/wp-abilities-verify/references/static-enumeration.md +126 -0
- package/.github/skills/wp-playground/SKILL.md +132 -1
- package/.github/skills/wp-playground/references/e2e-playwright.md +115 -0
- package/.github/skills/wp-plugin-directory-guidelines/SKILL.md +133 -0
- package/.github/skills/wp-plugin-directory-guidelines/references/gpl-compliance.md +217 -0
- package/.github/skills/wp-plugin-directory-guidelines/references/guideline-review-checklist.md +592 -0
- package/.github/skills/wp-plugin-directory-guidelines/references/naming-rules.md +121 -0
- package/.github/skills/wp-project-triage/scripts/detect_wp_project.mjs +22 -4
- package/.github/skills/wp-wpengine/SKILL.md +127 -0
- package/README.md +14 -8
- package/dist/lib/api.js +43 -19
- package/dist/lib/installer.js +0 -2
- package/extensions/wp-agent-kit/index.ts +146 -324
- package/package.json +1 -1
- package/skills-custom/wp-wpengine/SKILL.md +127 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Schema Lints
|
|
2
|
+
|
|
3
|
+
Static lints against an ability's `input_schema`. Schema hygiene is
|
|
4
|
+
about *agent legibility*: orchestrating agents read the schema to
|
|
5
|
+
figure out how to call the ability. A schema that's hard to parse,
|
|
6
|
+
ambiguous, or misleading wastes turns even when the ability itself
|
|
7
|
+
works.
|
|
8
|
+
|
|
9
|
+
These lints are six small principles. Apply them by reading the
|
|
10
|
+
schema, not by mechanically grepping — most plugins use enough
|
|
11
|
+
formatting variety that grep recipes drift.
|
|
12
|
+
|
|
13
|
+
## Lint 1 — `additionalProperties: false` for object schemas
|
|
14
|
+
|
|
15
|
+
For top-level `'type' => 'object'` schemas, declare
|
|
16
|
+
`'additionalProperties' => false` unless you deliberately accept
|
|
17
|
+
extras. Without this, an agent passing a typo (`par_page` instead of
|
|
18
|
+
`per_page`) gets accepted silently and falls through to the backing,
|
|
19
|
+
which ignores the unknown key.
|
|
20
|
+
|
|
21
|
+
- `additionalProperties: false` declared → OK.
|
|
22
|
+
- `additionalProperties: true` declared → WARN, unless the schema is
|
|
23
|
+
for genuinely free-form metadata (payment custom fields, form
|
|
24
|
+
free-text); document the reason inline.
|
|
25
|
+
- Not declared on an object schema → WARN.
|
|
26
|
+
- Non-object root (string with enum, integer, etc.) → N/A. The lint
|
|
27
|
+
applies only to objects.
|
|
28
|
+
|
|
29
|
+
## Lint 2 — every required field has a non-empty description
|
|
30
|
+
|
|
31
|
+
For each entry in `required`, the matching `properties` entry must
|
|
32
|
+
declare a non-empty `description`. Required fields are where agents
|
|
33
|
+
most need guidance; an opaque required key forces the agent to guess
|
|
34
|
+
from the field name alone. Empty / missing → FAIL.
|
|
35
|
+
|
|
36
|
+
Optional-field descriptions are nice-to-have — absence is WARN.
|
|
37
|
+
|
|
38
|
+
## Lint 3 — enums are non-empty
|
|
39
|
+
|
|
40
|
+
`'enum' => []` accepts no values, rejecting every input. Almost always
|
|
41
|
+
a bug. → FAIL.
|
|
42
|
+
|
|
43
|
+
A single-value enum (`'enum' => [ 'pending' ]`) is legal but unusual;
|
|
44
|
+
WARN and prompt for review — often a copy-paste that lost the other
|
|
45
|
+
values.
|
|
46
|
+
|
|
47
|
+
## Lint 4 — no `$ref`
|
|
48
|
+
|
|
49
|
+
Agents read the schema via REST introspection. A `$ref` forces the
|
|
50
|
+
agent to follow a reference to see the field shape — wastes a turn and
|
|
51
|
+
often breaks because the referenced schema isn't in the same document.
|
|
52
|
+
Inline the shape instead.
|
|
53
|
+
|
|
54
|
+
Any `'$ref'` in the schema → FAIL.
|
|
55
|
+
|
|
56
|
+
## Lint 5 — defaults are statically constant
|
|
57
|
+
|
|
58
|
+
Each `'default'` value must evaluate to the same shape on every call:
|
|
59
|
+
|
|
60
|
+
- Scalar literals — `true`, `false`, integer, float, quoted string,
|
|
61
|
+
`null` → OK.
|
|
62
|
+
- Empty or all-literal arrays — `[]`, `array()`, `[ 'a', 'b' ]` → OK.
|
|
63
|
+
- Literal cast to an empty object — `(object) array()`, `(object) []`
|
|
64
|
+
→ OK. This is the recommended top-level default for zero-arg-allowed
|
|
65
|
+
abilities; see
|
|
66
|
+
`../../wp-abilities-api/references/input-schema-gotchas.md` §4.
|
|
67
|
+
- `new stdClass()` with no arguments → OK.
|
|
68
|
+
- A function call (`gmdate('c')`, `wp_generate_uuid4()`, `time()`),
|
|
69
|
+
variable reference, or other computed expression → FAIL.
|
|
70
|
+
|
|
71
|
+
The principle: defaults that vary per call are both non-deterministic
|
|
72
|
+
and surprising to agents that expect defaults to be static.
|
|
73
|
+
|
|
74
|
+
## Lint 6 — `reference_ability: true` implies no required inputs
|
|
75
|
+
|
|
76
|
+
If an audit doc is provided and an ability has
|
|
77
|
+
`reference_ability: true`, its `input_schema.required` array must be
|
|
78
|
+
empty or absent. The reference ability is the smallest, safest
|
|
79
|
+
bootstrap call an implementer lands first; it must work with
|
|
80
|
+
`execute([])`. Required inputs on the reference ability → FAIL.
|
|
81
|
+
|
|
82
|
+
(No audit provided → this lint is skipped — no reference ability is
|
|
83
|
+
declared.)
|
|
84
|
+
|
|
85
|
+
## Cross-reference: gotchas 1-3 (callback hardening) and gotcha 4 (structural default)
|
|
86
|
+
|
|
87
|
+
Static lints catch shape; the four runtime gotchas in
|
|
88
|
+
`../../wp-abilities-api/references/input-schema-gotchas.md` split into
|
|
89
|
+
two kinds.
|
|
90
|
+
|
|
91
|
+
Gotchas 1-3 need defensive code in the execute callback —
|
|
92
|
+
`array_key_exists` instead of `isset`-only for property defaults,
|
|
93
|
+
pagination key translation, ID validation that accepts `"0"`. These
|
|
94
|
+
are runtime behaviors the callback itself must handle; static schema
|
|
95
|
+
lints can't enforce them.
|
|
96
|
+
|
|
97
|
+
Gotcha 4 — the direct vs indirect invocation strictness — is what
|
|
98
|
+
motivates the `(object) array()` top-level default that Lint 5
|
|
99
|
+
explicitly accepts. This one IS structural and Lint 5 carries the
|
|
100
|
+
enforcement.
|
|
101
|
+
|
|
102
|
+
## Output format
|
|
103
|
+
|
|
104
|
+
```markdown
|
|
105
|
+
## Schema lints
|
|
106
|
+
|
|
107
|
+
| Ability | Lint | Result | Detail |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| <ability> | additionalProperties (object schemas) | WARN | not declared on object schema |
|
|
110
|
+
| <ability> | required-field descriptions | OK | 3/3 required fields documented |
|
|
111
|
+
| <ability> | enum non-empty | OK | no enums |
|
|
112
|
+
| <ability> | no $ref | OK | inline |
|
|
113
|
+
| <ability> | static defaults | FAIL | `created_at` uses `gmdate('c')` |
|
|
114
|
+
| <ability> | reference_ability implies no required | N/A | not reference ability |
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A FAIL on any lint flips that ability to FAIL in the run summary.
|
|
118
|
+
WARNs surface but don't block.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Static Enumeration
|
|
2
|
+
|
|
3
|
+
Enumerate a plugin's abilities from source, with no running
|
|
4
|
+
environment. Static enumeration is necessarily best-effort — PHP's
|
|
5
|
+
dynamism (variable indirection, runtime-conditional registration)
|
|
6
|
+
means a complete inventory only comes from a live `wp_get_abilities()`
|
|
7
|
+
call. When static and runtime inventories diverge, trust runtime;
|
|
8
|
+
static drives the diff so the reviewer knows where to look.
|
|
9
|
+
|
|
10
|
+
## Typical registration shape
|
|
11
|
+
|
|
12
|
+
```php
|
|
13
|
+
wp_register_ability(
|
|
14
|
+
'<plugin-slug>/<ability-name>',
|
|
15
|
+
array(
|
|
16
|
+
'label' => __( '...', '<text-domain>' ),
|
|
17
|
+
'description' => __( '...', '<text-domain>' ),
|
|
18
|
+
'category' => '<category-slug>',
|
|
19
|
+
'input_schema' => array( /* ... */ ),
|
|
20
|
+
'execute_callback' => array( self::class, 'execute_my_ability' ),
|
|
21
|
+
'permission_callback' => array( self::class, 'check_permission' ),
|
|
22
|
+
'meta' => array(
|
|
23
|
+
'annotations' => array(
|
|
24
|
+
'readonly' => true,
|
|
25
|
+
'destructive' => false,
|
|
26
|
+
'idempotent' => true,
|
|
27
|
+
),
|
|
28
|
+
'show_in_rest' => true,
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Step 1 — find every registration call
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
grep -rn --include='*.php' 'wp_register_ability\s*(' <plugin-root>/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Zero hits → return a clear "no abilities registered" report per
|
|
41
|
+
SKILL.md "Failure modes." Don't fabricate an empty inventory.
|
|
42
|
+
|
|
43
|
+
## Step 2 — extract each ability name
|
|
44
|
+
|
|
45
|
+
The first argument is the ability name — usually a literal string.
|
|
46
|
+
Real-world formatting splits the call across lines (the example
|
|
47
|
+
above is itself multi-line), so single-line regexes miss common
|
|
48
|
+
cases. Use a multi-line tool:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# ripgrep with --multiline + PCRE2 captures the name regardless of line break:
|
|
52
|
+
rg --multiline --pcre2 --type=php -n \
|
|
53
|
+
"wp_register_ability\s*\(\s*['\"]([^'\"]+)['\"]" <plugin-root>/
|
|
54
|
+
|
|
55
|
+
# Fallbacks: pcregrep -M, perl -0777.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If the first argument is a variable or constant
|
|
59
|
+
(`wp_register_ability( $name, ... )`,
|
|
60
|
+
`wp_register_ability( MyPlugin\NAME, ... )`), trace it: a recent
|
|
61
|
+
assignment in the same function or a class constant usually resolves;
|
|
62
|
+
a name computed in a loop won't, in which case flag the limitation and
|
|
63
|
+
recommend runtime mode for authoritative enumeration.
|
|
64
|
+
|
|
65
|
+
## Step 3 — extract the annotation block
|
|
66
|
+
|
|
67
|
+
Annotations live at `meta.annotations.{readonly,destructive,idempotent}`.
|
|
68
|
+
For each registration, read the array literal forward until the
|
|
69
|
+
matching close. Common shapes:
|
|
70
|
+
|
|
71
|
+
- Multi-line literal (most common).
|
|
72
|
+
- Short-form one-liner.
|
|
73
|
+
- Helper method (`'annotations' => self::annotations_for_read()`) —
|
|
74
|
+
resolve the helper if it returns a literal; otherwise mark
|
|
75
|
+
`<unresolved>` and run the adversarial check against the callback
|
|
76
|
+
alone.
|
|
77
|
+
- Class constant (`'annotations' => self::READONLY_ANNOTATIONS`) —
|
|
78
|
+
resolve the constant.
|
|
79
|
+
|
|
80
|
+
Record `ability_name → declared_annotations`.
|
|
81
|
+
|
|
82
|
+
## Step 4 — follow `execute_callback` to its body
|
|
83
|
+
|
|
84
|
+
`execute_callback` is one of:
|
|
85
|
+
|
|
86
|
+
```php
|
|
87
|
+
'execute_callback' => array( self::class, 'execute_get_things' ),
|
|
88
|
+
'execute_callback' => array( My_Class::class, 'execute_get_things' ),
|
|
89
|
+
'execute_callback' => array( $this, 'execute_get_things' ),
|
|
90
|
+
'execute_callback' => 'my_plugin_execute_get_things', // top-level function
|
|
91
|
+
'execute_callback' => function ( $input ) { /* ... */ }, // closure
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Resolve the reference to its source location: file + start line + end
|
|
95
|
+
line. The annotation-correctness, schema-lint, and permission checks
|
|
96
|
+
all operate on that byte range.
|
|
97
|
+
|
|
98
|
+
## Limits of static enumeration
|
|
99
|
+
|
|
100
|
+
Cases where the inventory is incomplete or ambiguous:
|
|
101
|
+
|
|
102
|
+
- Variable-indirected names (`foreach` over a slug list).
|
|
103
|
+
- Variable-indirected annotations (built from config).
|
|
104
|
+
- Conditional registration (`if ( feature_enabled() )`).
|
|
105
|
+
- Variable-indirected callbacks (`array( $this, $callback_name )`).
|
|
106
|
+
|
|
107
|
+
Record each in the report's "Static enumeration limitations" section
|
|
108
|
+
and recommend a runtime-mode rerun for the authoritative inventory.
|
|
109
|
+
|
|
110
|
+
## Output format
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
## Static inventory
|
|
114
|
+
|
|
115
|
+
Found <N> ability registrations across <M> files:
|
|
116
|
+
|
|
117
|
+
| Ability | Source file | Registration line | Callback file | Callback lines |
|
|
118
|
+
|---|---|---|---|---|
|
|
119
|
+
| myplugin/get-foo | src/Abilities.php | 42 | src/Abilities.php | 102-134 |
|
|
120
|
+
| myplugin/submit-bar | src/Abilities.php | 68 | src/Services/Bar.php | 58-91 |
|
|
121
|
+
|
|
122
|
+
### Limitations
|
|
123
|
+
|
|
124
|
+
- <ability>: annotations built dynamically in `annotations_for_read()`;
|
|
125
|
+
recommend runtime mode for annotation cross-check.
|
|
126
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wp-playground
|
|
3
|
-
description: "Use for WordPress Playground workflows: fast disposable WP instances in the browser or locally via @wp-playground/cli (server, run-blueprint, build-snapshot), auto-mounting plugins/themes, switching WP/PHP versions, blueprints, and debugging (Xdebug)."
|
|
3
|
+
description: "Use for WordPress Playground workflows: fast disposable WP instances in the browser or locally via @wp-playground/cli (server, run-blueprint, build-snapshot), auto-mounting plugins/themes, switching WP/PHP versions, blueprints, PHPUnit testing, E2E Playwright testing, programmatic runCLI API, and debugging (Xdebug)."
|
|
4
4
|
license: GPL-2.0-or-later
|
|
5
5
|
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Playground CLI requires Node.js 20.18+; runs WP in WebAssembly with SQLite."
|
|
6
6
|
---
|
|
@@ -14,6 +14,9 @@ compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Playground CLI requires No
|
|
|
14
14
|
- Build a reproducible snapshot of a site for sharing or CI.
|
|
15
15
|
- Switch WP/PHP versions quickly to reproduce issues.
|
|
16
16
|
- Debug plugin/theme code with Xdebug in an isolated Playground.
|
|
17
|
+
- Run PHPUnit tests without a local database (no Docker, no MySQL).
|
|
18
|
+
- Write E2E Playwright tests against a real WordPress instance.
|
|
19
|
+
- Automate Playground in CI/CD pipelines via the programmatic `runCLI` API.
|
|
17
20
|
|
|
18
21
|
## Inputs required
|
|
19
22
|
|
|
@@ -82,6 +85,134 @@ npx @wp-playground/cli@latest build-snapshot --blueprint=<file> --outfile=./site
|
|
|
82
85
|
- Query: `https://playground.wordpress.net/?blueprint-url=<public-url-or-zip>`
|
|
83
86
|
- Use the live Blueprint Editor (playground.wordpress.net) to author blueprints with schema help; paste JSON and copy a shareable link.
|
|
84
87
|
|
|
88
|
+
### 8) PHPUnit testing (no database required)
|
|
89
|
+
|
|
90
|
+
Run PHPUnit inside Playground — every run starts with a clean WordPress install, fully isolated.
|
|
91
|
+
|
|
92
|
+
Plugin:
|
|
93
|
+
```bash
|
|
94
|
+
npx @wp-playground/cli@latest php \
|
|
95
|
+
--auto-mount \
|
|
96
|
+
-- \
|
|
97
|
+
/wordpress/wp-content/plugins/MY_PLUGIN/vendor/bin/phpunit \
|
|
98
|
+
-c /wordpress/wp-content/plugins/MY_PLUGIN/phpunit.xml.dist
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Theme (replace `plugins/` with `themes/`):
|
|
102
|
+
```bash
|
|
103
|
+
npx @wp-playground/cli@latest php \
|
|
104
|
+
--auto-mount \
|
|
105
|
+
-- \
|
|
106
|
+
/wordpress/wp-content/themes/MY_THEME/vendor/bin/phpunit \
|
|
107
|
+
-c /wordpress/wp-content/themes/MY_THEME/phpunit.xml.dist
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- Requires PHPUnit installed via Composer in the project (`composer require --dev phpunit/phpunit`).
|
|
111
|
+
- `--auto-mount` maps the current directory to the correct WP content path automatically.
|
|
112
|
+
- Use `--php=8.1 --wp=6.5` flags to test against specific versions (PHP 7.4–8.5 supported).
|
|
113
|
+
- Explicit mount: `--mount=.:/wordpress/wp-content/plugins/MY_PLUGIN`.
|
|
114
|
+
|
|
115
|
+
### 9) E2E testing with Playwright
|
|
116
|
+
|
|
117
|
+
Use `runCLI` from `@wp-playground/cli` to start a Playground server programmatically in Playwright tests. No Docker, no database.
|
|
118
|
+
|
|
119
|
+
Install:
|
|
120
|
+
```bash
|
|
121
|
+
npm install --save-dev @playwright/test @wp-playground/cli
|
|
122
|
+
npx playwright install chromium
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Minimal `playwright.config.ts`:
|
|
126
|
+
```ts
|
|
127
|
+
import { defineConfig } from '@playwright/test';
|
|
128
|
+
export default defineConfig({
|
|
129
|
+
testDir: './tests/e2e',
|
|
130
|
+
fullyParallel: false,
|
|
131
|
+
workers: 1, // prevent port conflicts
|
|
132
|
+
timeout: 120_000, // WP boot takes time
|
|
133
|
+
expect: { timeout: 30_000 },
|
|
134
|
+
use: { screenshot: 'only-on-failure', trace: 'on-first-retry' },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
First test (`tests/e2e/plugin.spec.ts`):
|
|
139
|
+
```ts
|
|
140
|
+
import { test, expect } from '@playwright/test';
|
|
141
|
+
import { runCLI } from '@wp-playground/cli';
|
|
142
|
+
|
|
143
|
+
let cli: Awaited<ReturnType<typeof runCLI>>;
|
|
144
|
+
|
|
145
|
+
test.beforeAll(async () => {
|
|
146
|
+
cli = await runCLI({
|
|
147
|
+
command: 'server',
|
|
148
|
+
blueprint: {
|
|
149
|
+
preferredVersions: { php: '8.3', wp: 'latest' },
|
|
150
|
+
login: true,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test.afterAll(async () => { await cli?.server?.close(); });
|
|
156
|
+
|
|
157
|
+
test('dashboard loads', async ({ page }) => {
|
|
158
|
+
await page.goto(`${cli.serverUrl}/wp-admin/`);
|
|
159
|
+
await expect(page.locator('#wpbody-content')).toBeVisible();
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Mount a local plugin:
|
|
164
|
+
```ts
|
|
165
|
+
cli = await runCLI({
|
|
166
|
+
command: 'server',
|
|
167
|
+
mount: { './': '/wordpress/wp-content/plugins/my-plugin' },
|
|
168
|
+
blueprint: {
|
|
169
|
+
preferredVersions: { php: '8.3', wp: 'latest' },
|
|
170
|
+
login: true,
|
|
171
|
+
steps: [{ step: 'activatePlugin', pluginPath: 'my-plugin/my-plugin.php' }],
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Locator priority (most to least preferred):
|
|
177
|
+
1. `page.getByRole()` — buttons, headings, links, inputs
|
|
178
|
+
2. `page.getByLabel()` — labeled form fields
|
|
179
|
+
3. `page.getByText()` — visible text
|
|
180
|
+
4. `page.getByTestId()` — your own `data-testid` attributes
|
|
181
|
+
5. `page.locator()` — CSS/XPath last resort (WP core layout elements like `#wpadminbar`)
|
|
182
|
+
|
|
183
|
+
Version matrix pattern:
|
|
184
|
+
```ts
|
|
185
|
+
const matrix = [{ php: '8.1', wp: '6.5' }, { php: '8.3', wp: 'latest' }];
|
|
186
|
+
for (const { php, wp } of matrix) {
|
|
187
|
+
test.describe(`PHP ${php} + WP ${wp}`, () => {
|
|
188
|
+
// ... beforeAll/afterAll with those versions
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
CI (GitHub Actions): see `references/e2e-playwright.md`.
|
|
194
|
+
|
|
195
|
+
### 10) Programmatic `runCLI` API (scripts / Vitest integration)
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { runCLI } from '@wp-playground/cli';
|
|
199
|
+
|
|
200
|
+
const server = await runCLI({
|
|
201
|
+
command: 'server',
|
|
202
|
+
php: '8.3',
|
|
203
|
+
wp: 'latest',
|
|
204
|
+
login: true,
|
|
205
|
+
mount: [{ hostPath: './my-plugin', vfsPath: '/wordpress/wp-content/plugins/my-plugin' }],
|
|
206
|
+
blueprint: { steps: [{ step: 'activatePlugin', pluginPath: 'my-plugin/plugin.php' }] },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// server.serverUrl — the base URL
|
|
210
|
+
// server.server — the HTTP server instance (call .close() in afterEach)
|
|
211
|
+
// server[Symbol.asyncDispose]() — cleanup in Vitest afterEach
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Use `wordpressInstallMode: 'do-not-attempt-installing'` + `skipSqliteSetup: true` when testing pure PHP with no WordPress overhead.
|
|
215
|
+
|
|
85
216
|
## Verification
|
|
86
217
|
|
|
87
218
|
- Verify mounted code is active (plugin listed/active; theme selected).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# E2E Testing with Playwright + WordPress Playground
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install --save-dev @playwright/test @wp-playground/cli
|
|
7
|
+
npx playwright install chromium
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## playwright.config.ts
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { defineConfig } from '@playwright/test';
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
testDir: './tests/e2e',
|
|
16
|
+
fullyParallel: false,
|
|
17
|
+
forbidOnly: !!process.env.CI,
|
|
18
|
+
retries: process.env.CI ? 2 : 0,
|
|
19
|
+
workers: 1,
|
|
20
|
+
reporter: 'html',
|
|
21
|
+
timeout: 120_000,
|
|
22
|
+
expect: { timeout: 30_000 },
|
|
23
|
+
use: { screenshot: 'only-on-failure', trace: 'on-first-retry' },
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Server lifecycle
|
|
28
|
+
|
|
29
|
+
**Shared** (faster, tests can affect each other — use for read-only tests):
|
|
30
|
+
```ts
|
|
31
|
+
test.beforeAll(async () => { cli = await runCLI({ command: 'server', blueprint }); });
|
|
32
|
+
test.afterAll(async () => { await cli?.server?.close(); });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Per-test** (isolated, slower — use when tests modify state):
|
|
36
|
+
```ts
|
|
37
|
+
test.beforeEach(async () => { cli = await runCLI({ command: 'server', blueprint }); });
|
|
38
|
+
test.afterEach(async () => { await cli?.server?.close(); });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Blueprint fixtures
|
|
42
|
+
|
|
43
|
+
Installing from wordpress.org:
|
|
44
|
+
```ts
|
|
45
|
+
steps: [{ step: 'installPlugin', pluginData: { resource: 'wordpress.org/plugins', slug: 'contact-form-7' } }]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Creating content:
|
|
49
|
+
```ts
|
|
50
|
+
steps: [{ step: 'runPHP', code: `<?php require '/wordpress/wp-load.php'; wp_insert_post([...]);` }]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## WordPress-specific locator guidance
|
|
54
|
+
|
|
55
|
+
- `page.getByRole('button', { name: 'Save Changes' })` — works for standard WP buttons
|
|
56
|
+
- `page.getByLabel('API Key')` — works for labeled form fields
|
|
57
|
+
- `page.locator('#wpadminbar')` — CSS required for WP core layout elements (no ARIA)
|
|
58
|
+
- Add `data-testid` to your own plugin markup for stable selectors
|
|
59
|
+
- Run `npx playwright codegen localhost:9400/wp-admin/` to auto-generate locators
|
|
60
|
+
|
|
61
|
+
## Page Object Model
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// tests/e2e/pages/plugin-settings.ts
|
|
65
|
+
export class PluginSettingsPage {
|
|
66
|
+
constructor(readonly page: Page) {}
|
|
67
|
+
async goto(baseUrl: string) { await this.page.goto(`${baseUrl}/wp-admin/options-general.php?page=my-plugin`); }
|
|
68
|
+
async setApiKey(key: string) {
|
|
69
|
+
await this.page.getByLabel('API Key').fill(key);
|
|
70
|
+
await this.page.getByRole('button', { name: 'Save Changes' }).click();
|
|
71
|
+
}
|
|
72
|
+
async expectSaved() { await expect(this.page.getByText('Settings saved')).toBeVisible(); }
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## GitHub Actions CI
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
name: E2E Tests
|
|
80
|
+
on:
|
|
81
|
+
push: { branches: [main] }
|
|
82
|
+
pull_request: { branches: [main] }
|
|
83
|
+
jobs:
|
|
84
|
+
e2e:
|
|
85
|
+
runs-on: ubuntu-latest
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v4
|
|
88
|
+
- uses: actions/setup-node@v4
|
|
89
|
+
with: { node-version: 20, cache: 'npm' }
|
|
90
|
+
- run: npm ci
|
|
91
|
+
- uses: actions/cache@v4
|
|
92
|
+
id: playwright-cache
|
|
93
|
+
with:
|
|
94
|
+
path: ~/.cache/ms-playwright
|
|
95
|
+
key: playwright-${{ hashFiles('package-lock.json') }}
|
|
96
|
+
- if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
97
|
+
run: npx playwright install chromium --with-deps
|
|
98
|
+
- run: npx playwright test
|
|
99
|
+
- uses: actions/upload-artifact@v4
|
|
100
|
+
if: ${{ !cancelled() }}
|
|
101
|
+
with: { name: playwright-report, path: playwright-report/, retention-days: 30 }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Troubleshooting
|
|
105
|
+
|
|
106
|
+
- **Timeout errors** — increase `timeout` in config; CI needs 120–180s
|
|
107
|
+
- **Port conflicts** — don't hardcode ports; use `cli.serverUrl`
|
|
108
|
+
- **Browser not found** — run `npx playwright install chromium`
|
|
109
|
+
- **Passes locally, fails CI** — increase timeouts, ensure `workers: 1`
|
|
110
|
+
- **Debug** — `npx playwright test --debug` or `--ui` for interactive mode
|
|
111
|
+
|
|
112
|
+
## Docs
|
|
113
|
+
|
|
114
|
+
- https://wordpress.github.io/wordpress-playground/guides/e2e-testing-with-playwright
|
|
115
|
+
- https://wordpress.github.io/wordpress-playground/guides/programmatic-playground-cli
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-plugin-directory-guidelines
|
|
3
|
+
description: "Use when reviewing WordPress plugins for GPL compliance, checking license headers or compatibility, evaluating upsell/freemium/trialware patterns, validating plugin naming or trademark rules, checking plugin slugs, understanding why a plugin was rejected from WordPress.org, or answering any question about the 18 WordPress.org Plugin Directory guidelines — even if the user doesn't mention 'guidelines' explicitly."
|
|
4
|
+
license: GPL-2.0-or-later
|
|
5
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+)."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Authoritative reference for the 18 WordPress.org Plugin Directory guidelines. Covers GPL licensing, plugin naming/trademark rules, trialware restrictions, and all other submission requirements.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
Use this skill when you need to:
|
|
15
|
+
- Review a WordPress plugin for compliance with the WordPress.org Plugin Directory guidelines
|
|
16
|
+
- Check GPL license compatibility for a plugin or its bundled libraries
|
|
17
|
+
- Verify license headers in plugin files
|
|
18
|
+
- Identify common guideline violations before submission
|
|
19
|
+
- Answer questions about what is or is not allowed on WordPress.org
|
|
20
|
+
- Evaluate premium/upsell flows, license checks, or freemium positioning
|
|
21
|
+
- Review "teaser" or "preview" UI for trialware violations
|
|
22
|
+
|
|
23
|
+
## Inputs required
|
|
24
|
+
|
|
25
|
+
- Plugin source code (or specific files to review)
|
|
26
|
+
- Optional: plugin readme and plugin header metadata for naming and license checks
|
|
27
|
+
|
|
28
|
+
## Procedure
|
|
29
|
+
|
|
30
|
+
1. Check the plugin's license header against the **Valid License Headers** section below.
|
|
31
|
+
2. Walk through the **18 Guidelines** checklist, paying special attention to Guidelines 1, 4, 5, 7, 8, and 17.
|
|
32
|
+
3. Confirm trialware/freemium compliance using the checklist in [guideline-review-checklist.md](references/guideline-review-checklist.md) (Guideline 5 section).
|
|
33
|
+
4. For bundled third-party code, verify license compatibility against **GPL-Compatible Licenses (Quick)** below.
|
|
34
|
+
5. Flag matches from **Common GPL Violations (Quick)** below.
|
|
35
|
+
6. For edge cases, consult the detailed references and the [GNU GPL FAQ](https://www.gnu.org/licenses/gpl-faq.html).
|
|
36
|
+
|
|
37
|
+
## 18-Guideline Review Checklist
|
|
38
|
+
|
|
39
|
+
Use the detailed, per-guideline checklist in [guideline-review-checklist.md](references/guideline-review-checklist.md). Load this reference file only when a full guideline audit is requested.
|
|
40
|
+
|
|
41
|
+
## GPL Compliance (Guideline 1 in Detail)
|
|
42
|
+
|
|
43
|
+
Use [gpl-compliance.md](references/gpl-compliance.md) for full license tables, compatibility nuances, and examples. Keep this inline section as a quick decision aid.
|
|
44
|
+
|
|
45
|
+
### Verification (Licensing)
|
|
46
|
+
|
|
47
|
+
- Every licensing-related issue must cite **Guideline 1** and include the file path and exact license string.
|
|
48
|
+
- Confirm compatibility claims against **GPL-Compatible Licenses (Quick)** and escalate ambiguous licenses.
|
|
49
|
+
|
|
50
|
+
### Failure modes (Licensing)
|
|
51
|
+
|
|
52
|
+
- If a license is not clearly GPL-compatible, do not guess. Check the [GNU license list](https://www.gnu.org/licenses/license-list.html).
|
|
53
|
+
- For dual-license packages, verify both licenses and redistribution terms.
|
|
54
|
+
|
|
55
|
+
### Quick Reference: WordPress GPL Requirements
|
|
56
|
+
|
|
57
|
+
- WordPress is **GPLv2 or later**.
|
|
58
|
+
- Plugins distributed on WordPress.org must be 100% GPL-compatible (code and assets).
|
|
59
|
+
- Include a valid `License:` header and `License URI:` in the main plugin file.
|
|
60
|
+
- Do not add restrictions that conflict with GPL freedoms.
|
|
61
|
+
|
|
62
|
+
### Valid License Headers
|
|
63
|
+
|
|
64
|
+
## GPL Versions Summary
|
|
65
|
+
|
|
66
|
+
| Version | Year | Key Addition |
|
|
67
|
+
|---------|------|--------------|
|
|
68
|
+
| GPLv1 | 1989 | Base copyleft: share-alike for modifications |
|
|
69
|
+
| GPLv2 | 1991 | "Liberty or death" clause (Section 7), clearer distribution terms |
|
|
70
|
+
| GPLv3 | 2007 | Anti-tivoization, explicit patent grants, compatibility provisions |
|
|
71
|
+
|
|
72
|
+
WordPress uses **GPLv2 or later**, meaning plugins can use GPLv2, GPLv3, or "GPLv2 or later".
|
|
73
|
+
|
|
74
|
+
For full license texts, see:
|
|
75
|
+
- [GNU General Public License v1](https://www.gnu.org/licenses/gpl-1.0.html)
|
|
76
|
+
- [GNU General Public License v2](https://www.gnu.org/licenses/gpl-2.0.html)
|
|
77
|
+
- [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html)
|
|
78
|
+
|
|
79
|
+
## License Compliance Checklist
|
|
80
|
+
|
|
81
|
+
When reviewing a plugin, verify:
|
|
82
|
+
|
|
83
|
+
- [ ] Main plugin file has a valid `License:` header (e.g., `GPL-2.0-or-later`, `GPL-2.0+`, `GPLv2 or later`)
|
|
84
|
+
- [ ] Main plugin file has a `License URI:` header pointing to the GPL text
|
|
85
|
+
- [ ] If bundled libraries exist, each has a GPL-compatible license
|
|
86
|
+
- [ ] No "split licensing" (e.g., code GPL but premium features proprietary)
|
|
87
|
+
- [ ] No additional restrictions beyond what GPL allows
|
|
88
|
+
- [ ] No clauses restricting commercial use, modification, or redistribution
|
|
89
|
+
- [ ] No obfuscated code (violates the spirit of source code availability)
|
|
90
|
+
|
|
91
|
+
## Valid License Headers for WordPress Plugins
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
License: GPL-2.0-or-later
|
|
95
|
+
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
License: GPL-3.0-or-later
|
|
100
|
+
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
License: GPLv2 or later
|
|
105
|
+
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### GPL-Compatible Licenses (Quick)
|
|
109
|
+
|
|
110
|
+
- Safe defaults: GPL-2.0-or-later, GPL-3.0-or-later.
|
|
111
|
+
- Commonly accepted permissive families: MIT/Expat, BSD, ISC, zlib, Boost.
|
|
112
|
+
- Conditional compatibility requires care: Apache-2.0 and MPL-2.0 (verify usage context).
|
|
113
|
+
- For full accepted and rejected identifiers, use [gpl-compliance.md](references/gpl-compliance.md).
|
|
114
|
+
|
|
115
|
+
### Common GPL Violations (Quick)
|
|
116
|
+
|
|
117
|
+
- Split licensing that restricts distributed code.
|
|
118
|
+
- Obfuscated or non-corresponding source distribution.
|
|
119
|
+
- Restrictive clauses (non-commercial, no-resale, forced backlink).
|
|
120
|
+
- Bundling GPL-incompatible libraries or assets.
|
|
121
|
+
|
|
122
|
+
## Plugin Naming Rules (Guideline 17)
|
|
123
|
+
|
|
124
|
+
Use [naming-rules.md](references/naming-rules.md) for full trademark lists, slug blocks, and naming examples. Keep this inline checklist for quick screening.
|
|
125
|
+
|
|
126
|
+
### Naming Checklist (Quick)
|
|
127
|
+
|
|
128
|
+
- Name is not a placeholder and has at least 5 alphanumeric characters.
|
|
129
|
+
- Header name and readme name match.
|
|
130
|
+
- Name is specific and function-related; avoid keyword stuffing.
|
|
131
|
+
- Trademark/project names appear only after connectors like `for`, `with`, `using`, `and`.
|
|
132
|
+
- No banned/discouraged terms or trademark portmanteaus.
|
|
133
|
+
- Slug is lowercase, hyphenated, <= 50 chars, and avoids blocked terms.
|