@pressgang-wp/shakedown 0.1.1 → 0.1.3

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 CHANGED
@@ -45,7 +45,7 @@ Optional `shakedown.config.json` in the theme, for overrides only:
45
45
 
46
46
  Authored journeys (form submissions, checkout flows) live in your theme's `tests/e2e/` — when present they run alongside the derived passes as the `journeys` project.
47
47
 
48
- **Seeding is convention-first.** If your theme ships [Muster](https://github.com/pressgang-wp/pressgang-muster) seeders (`src/Muster/*`), the sandbox runs them via `wp capstan seed` as the baseline — your real menus, terms and pages — then layers the derived populated/minimal ACF state fixtures on top. A theme that ships no seeders is unaffected: the derived layer covers it on its own, so it does a good job out of the box.
48
+ **Seeding is convention-first.** If your theme ships [Muster](https://github.com/pressgang-wp/pressgang-muster) seeders (a top-level `muster/` directory), the sandbox runs them via `wp capstan seed` as the baseline — your real menus, terms and pages — then layers the derived populated/minimal ACF state fixtures on top. A theme that ships no seeders is unaffected: the derived layer covers it on its own, so it does a good job out of the box.
49
49
 
50
50
  Sandbox fixture randomness and time are separate deterministic inputs. `seed`
51
51
  controls generated values; `epoch` fixes relative dates used by Muster,
@@ -4,11 +4,16 @@
4
4
  * the sandbox isolation witness has already proven this WordPress runs on
5
5
  * throwaway SQLite before this script is invoked).
6
6
  *
7
- * For every ACF field group with a seedable location target, creates two
8
- * published fixtures via Muster:
9
- * - populated: every generatable field filled (deterministic, seeded)
10
- * - minimal: required fields only — the sparsest state an editor can
11
- * legally publish, where empty-link/missing-image bugs live
7
+ * Each ACF field group is seeded according to where it is located:
8
+ * - post types, page and post templates → a dedicated PAIR of fixtures,
9
+ * `populated` (every generatable field filled) and `minimal` (required
10
+ * fields only — the sparsest publishable state, where empty-link and
11
+ * missing-image bugs live), each with a placeholder featured image;
12
+ * - options pages → the populated values written once (global chrome);
13
+ * - the front page (`page_type=front_page`) → the populated values onto the
14
+ * home page, reusing a theme-assigned front page or creating one;
15
+ * - nav-menu-item groups → left to the theme's own seeder, whose menus carry
16
+ * the real structure the fields attach to.
12
17
  *
13
18
  * Run via: wp eval-file bin/seed-states.php <muster-autoload> <acf-json-dir> <seed> <epoch>
14
19
  * Emits JSON: {"routes": [{url, kind, expect}...]} for the matrix.
@@ -51,13 +56,23 @@ $generator = new AcfValueGenerator($context->victuals(), [
51
56
  ->id(),
52
57
  'post' => function (array $types) use ($context): int {
53
58
  $type = $types[0] ?? 'post';
59
+ $slug = 'state-related-' . sanitize_title($type);
54
60
 
55
- return (new PostBuilder($context, $type))
61
+ $ref = (new PostBuilder($context, $type))
56
62
  ->title('State related ' . $type)
57
- ->slug('state-related-' . sanitize_title($type))
63
+ ->slug($slug)
58
64
  ->status('publish')
59
- ->save()
60
- ->id();
65
+ // Pin before the fixture epoch so relationship stubs sink below the
66
+ // real fixtures in date-ordered feeds rather than heading them.
67
+ ->date($context->clock()->epoch()->modify('-1 year')->format('Y-m-d H:i:s'))
68
+ ->save();
69
+
70
+ (new AttachmentBuilder($context, "{$slug}-thumb"))
71
+ ->placeholder(1200, 800)
72
+ ->featuredOn($ref)
73
+ ->save();
74
+
75
+ return $ref->id();
61
76
  },
62
77
  'term' => fn (string $taxonomy): int => (new TermBuilder($context, $taxonomy))
63
78
  ->name('State term')
@@ -68,43 +83,90 @@ $generator = new AcfValueGenerator($context->victuals(), [
68
83
  ]);
69
84
 
70
85
  $routes = [];
86
+ $liveAcf = new LiveAcfAdapter();
71
87
 
72
88
  foreach (AcfJson::groups($acfJsonDir) as $group) {
73
89
  $targets = AcfJson::targets($group);
74
90
 
75
91
  if ($targets === []) {
76
- continue; // menu items, page_type rules not seedable (v1)
92
+ continue; // no seedable location (taxonomy terms, user roles, )
77
93
  }
78
94
 
79
95
  $target = $targets[0];
96
+ $fields = (array) $group['fields'];
97
+ $slugBase = sanitize_title($group['title'] ?? $group['key']);
80
98
 
81
99
  // Options-page groups are global state, not a URL: seed the populated
82
- // values once so chrome (header/footer) renders fully. The unseeded
83
- // fresh install already exercises the empty-options state.
100
+ // values once so chrome (header/footer) renders fully. The unseeded fresh
101
+ // install already exercises the empty-options state.
84
102
  if ($target['param'] === 'options_page') {
85
- (new LiveAcfAdapter())->updateFields($generator->populated((array) $group['fields']), 'option', 0);
103
+ $liveAcf->updateFields($generator->populated($fields), 'option', 0);
86
104
  continue;
87
105
  }
88
- $slugBase = sanitize_title($group['title'] ?? $group['key']);
89
106
 
107
+ // The front page is a singular surface — seed the populated state onto the
108
+ // one home page. Reuse the front page a theme seeder may already have set;
109
+ // assign one only when none exists, so we never fight a theme-authored home.
110
+ // The matrix's own `/` route covers it, so no dedicated route is emitted.
111
+ // Other page_type values (top_level, …) have no generic surface to seed.
112
+ if ($target['param'] === 'page_type') {
113
+ if ($target['value'] !== 'front_page') {
114
+ continue;
115
+ }
116
+
117
+ $frontId = (int) get_option('page_on_front');
118
+ if ($frontId === 0) {
119
+ $frontId = (new PostBuilder($context, 'page'))
120
+ ->title('Home')
121
+ ->slug('state-front-page')
122
+ ->status('publish')
123
+ ->date($fixtureDate)
124
+ ->save()
125
+ ->id();
126
+ update_option('show_on_front', 'page');
127
+ update_option('page_on_front', $frontId);
128
+ }
129
+
130
+ $liveAcf->updateFields($generator->populated($fields), 'post', $frontId);
131
+ continue;
132
+ }
133
+
134
+ // nav_menu_item groups attach to a menu's items, which exist only once a
135
+ // menu is built with the theme's real structure — that is the theme
136
+ // seeder's job. The derived path leaves them to the baseline rather than
137
+ // inventing a menu here.
138
+ if ($target['param'] === 'nav_menu_item') {
139
+ continue;
140
+ }
141
+
142
+ // Per-instance surfaces (post types, page and post templates): a dedicated
143
+ // populated AND minimal fixture, so the sparsest publishable state is
144
+ // exercised alongside the full one.
90
145
  foreach (['populated', 'minimal'] as $variant) {
91
- $fields = (array) $group['fields'];
92
146
  $values = $variant === 'populated' ? $generator->populated($fields) : $generator->minimal($fields);
93
147
 
94
- $builder = $target['param'] === 'page_template'
95
- ? (new PostBuilder($context, 'page'))->template($target['value'])
96
- : new PostBuilder($context, $target['value']);
148
+ $builder = match ($target['param']) {
149
+ 'page_template' => (new PostBuilder($context, 'page'))->template($target['value']),
150
+ 'post_template' => (new PostBuilder($context, 'post'))->template($target['value']),
151
+ default => new PostBuilder($context, $target['value']),
152
+ };
97
153
 
98
154
  $ref = $builder
99
155
  ->title(($group['title'] ?? $group['key']) . ' — ' . $variant)
100
156
  ->slug("state-{$slugBase}-{$variant}")
101
157
  ->status('publish')
102
158
  ->date($fixtureDate)
103
-
104
159
  ->content('State fixture: ' . $slugBase . ' (' . $variant . ')')
105
160
  ->acf($values)
106
161
  ->save();
107
162
 
163
+ // A placeholder featured image so archive and card thumbnails render
164
+ // instead of the theme's empty-thumbnail fallback.
165
+ (new AttachmentBuilder($context, "state-thumb-{$slugBase}-{$variant}"))
166
+ ->placeholder(1200, 800)
167
+ ->featuredOn($ref)
168
+ ->save();
169
+
108
170
  $url = get_permalink($ref->id());
109
171
 
110
172
  if ($url) {
package/lib/sandbox.mjs CHANGED
@@ -285,6 +285,12 @@ export async function bootSandbox(target, options = {}) {
285
285
  }
286
286
  }
287
287
 
288
+ // Clear WordPress's install defaults (Hello world!, Sample Page, the seeded
289
+ // comment) so the sandbox starts from a deterministic empty state — only
290
+ // seeded fixtures exist, and no version-dependent defaults leak into feeds,
291
+ // archives, or menus.
292
+ wp(root, ['site', 'empty', '--yes']);
293
+
288
294
  wp(root, ['rewrite', 'structure', '/%postname%/', '--hard']);
289
295
 
290
296
  const server = spawn('wp', ['server', `--host=127.0.0.1`, `--port=${port}`, `--docroot=${docroot}`, `--path=${root}`], {
@@ -308,7 +314,7 @@ export async function bootSandbox(target, options = {}) {
308
314
 
309
315
  /**
310
316
  * Convention-first baseline seeding: run the theme's OWN Muster seeders
311
- * (anything under its `src/Muster/`) via `wp capstan seed`, so a theme that
317
+ * (anything under its `muster/` directory) via `wp capstan seed`, so a theme that
312
318
  * ships fixtures gets its real content — menus assigned to locations, terms,
313
319
  * pages, relationships — exactly as `wp capstan seed` produces on a dev site.
314
320
  * The ACF state fixtures (see {@link seedAcfStates}) then layer on top.
@@ -323,7 +329,8 @@ export async function bootSandbox(target, options = {}) {
323
329
  * @returns {boolean} True when theme seeders ran; false when none were applied.
324
330
  */
325
331
  export function seedThemeMuster(sandbox, { seed = 42 } = {}) {
326
- const musterDir = join(sandbox.root, 'wp-content', 'themes', sandbox.theme ?? '', 'src', 'Muster');
332
+ // Seeders live in a top-level `muster/` directory (mapped under autoload-dev).
333
+ const musterDir = join(sandbox.root, 'wp-content', 'themes', sandbox.theme ?? '', 'muster');
327
334
 
328
335
  if (!sandbox.theme || !existsSync(musterDir)) {
329
336
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pressgang-wp/shakedown",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Sea trials for PressGang themes — config-derived e2e smoke testing",
5
5
  "type": "module",
6
6
  "license": "MIT",