@pressgang-wp/shakedown 0.1.1 → 0.1.2

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.
@@ -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}`], {
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.2",
4
4
  "description": "Sea trials for PressGang themes — config-derived e2e smoke testing",
5
5
  "type": "module",
6
6
  "license": "MIT",