test: Theme Builder fixtures with Pods fields from the real builder (stage3b)

Own fixture pod lit_fixture_person (ruling Q1), template and body layout
built through Divi's Theme Builder and Visual Builder UI, brief render with
the template limited to one person (Q5), findings 10-12 in the README.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 14:51:19 +02:00
co-authored by Claude Opus 5.5
parent 0edb474d78
commit c615522cf6
15 changed files with 494 additions and 7 deletions
+2
View File
@@ -95,6 +95,8 @@ ddev wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-st
Exportiert werden nur Posts mit Titel „[Fixture] …“ und die Standardvorlage, wenn sie auf Fixture-Layouts zeigt. Das Skript löscht vorher alle `*.json`/`*.html` in diesem Ordner. Exportiert werden nur Posts mit Titel „[Fixture] …“ und die Standardvorlage, wenn sie auf Fixture-Layouts zeigt. Das Skript löscht vorher alle `*.json`/`*.html` in diesem Ordner.
Seit den Etappe-3b-Fixtures (`stage3b/`, 2026-09-27) steht deren Vorlage 2394 ebenfalls in der Live-Liste; ein neuer Export ergäbe ein anderes `theme-builder.json`. Diesen Ordner daher nur auf einer Site ohne die 3b-Fixtures neu exportieren (Ruling B21).
## Aufräumen (lokal) ## Aufräumen (lokal)
Die Daten bleiben vorerst zum Ansehen stehen. Zum Entfernen (DDEV, WordPress-Wurzel; IDs vorher mit `manifest.json` abgleichen): Die Daten bleiben vorerst zum Ansehen stehen. Zum Entfernen (DDEV, WordPress-Wurzel; IDs vorher mit `manifest.json` abgleichen):
+111
View File
@@ -0,0 +1,111 @@
# Divi-5-Fixtures für Etappe 3b (Theme Builder mit Pods)
Echte Daten aus Divi 5.2.1 und Pods 3.3.9.2, erzeugt am 2026-09-27 auf der lokalen DDEV-Site (`https://wordpress.ddev.site:8443`). Vorlage und Body-Layout sind **über die Oberfläche** entstanden: echter Theme-Builder-Editor und der darin geöffnete Visual Builder in einem headless Chrome (per DevTools-Protokoll gesteuert, echte Mausklicks und Tastatureingaben, angemeldet als Administrator). Anders als in `stage3/` wurde kein Inhalt per `setContent` gesetzt und keine TB-Nutzlast nachgebaut: Module, Dynamic Content, Admin-Labels, Titel, Bedingungen und der Aktiv-Schalter kamen aus Divis eigenen Dialogen. Plugin-Code hat nichts geschrieben.
Pfade zu Divi relativ zu `wp-content/themes/Divi/includes/`. Die TB-App ist `builder/frontend-builder/build/theme-builder.js` (minifiziert, dort steht ein Suchbegriff statt einer Zeile).
## Dateien
Aufbau wie `stage3/`: je Post `<rolle>.json` (Post-Zeile, alle Meta roh in Speicherreihenfolge, Terme) und `<rolle>.html` mit `post_content` byte-genau; `manifest.json` (Rolle → ID), `theme-builder.json` (Live-TB-Post mit Vorlagenliste, `et_tb_*`-Optionen, als ungenutzt markierte Posts), dazu `render--person-erika.txt` (sichtbare Ausgabe, siehe Schritt 4).
| Rolle | ID | Typ | Inhalt |
|---|---|---|---|
| `lit_fixture_person--erika-muster` | 2385 | `lit_fixture_person` | `role` = „Geschäftsführung“, `level` = `senior`, kein Mentor |
| `lit_fixture_person--max-beispiel` | 2386 | `lit_fixture_person` | `role` = „Entwicklung“, `level` = `junior`, `mentor` = 2385 |
| `tb-body--person-body` | 2391 | `et_body_layout` | Überschrift „Person Titel“ (Beitragstitel), Texte „Person Funktion“/„Person Stufe“/„Person Mentor“ (Pods `role`/`level`/`mentor`), Modul „Person Beitragsinhalt“ (`divi/post-content`) |
| `tb-template--person` | 2394 | `et_template` | „[Fixture 3b] Person“, **deaktiviert**, Header = globaler Header 1061, Body = 2391, kein Footer, Bedingung `singular:post_type:lit_fixture_person:all` |
| (theme-builder.json) | 1064 | `et_theme_builder` | Live-TB-Post, `_et_template` = [1065, 1066, 2394] |
Nicht exportiert: die Pods-Definition (Pod 2381, Felder 2382–2384), die Revisionen 2392/2393 des Body-Layouts, die Standardvorlage 1065 und der Header 1061 (liegen in `stage3/`).
## Wie die Fixtures entstanden sind
### 1. Pod und Personen (Ruling Q1)
`wp eval-file …/tests/Integration/pods-fixture.php fixture` legt den eigenen Pod `lit_fixture_person` („[Fixture] Personen“, Post-Typ, Speicherung `meta`, öffentlich) mit `role` (Text), `level` (Auswahl `junior|Junior`, `senior|Senior`) und `mentor` (Beziehung auf `lit_fixture_person`, einzeln) an und danach die zwei Personen über `pods()->add()`. Der Aufruf ist wiederholbar (legt nichts doppelt an). `lit_person` (Pod der wp-basis-smoke) bleibt unberührt; `pods-fixture.php delete` löscht nur `lit_person`. Personen haben `post_author` 0, weil WP-CLI ohne Benutzer lief.
`et_builder_enabled_for_post_type('lit_fixture_person')` liefert „divi aus“. Die Divi-Einstellung „Beitragstyp-Integration“ wurde **nicht** geändert (Ruling Q5); der Theme Builder braucht sie nicht, die Bedingung „[Fixture] Personen“ bot er trotzdem an.
Vor dem ersten Live-Speichern gab es keine verwaisten TB-Posts: `_et_template` von 1064 war [1065, 1066], kein Post trug `_et_theme_builder_marked_as_unused` (B5).
### 2. Vorlage im Theme-Builder-Editor (`wp-admin/admin.php?page=et_theme_builder`)
1. „Neue Vorlage hinzufügen“ → „Neue Vorlage erstellen“ → im Dialog „Template-Einstellungen“ unter „[Fixture] Personen“ den Haken „Alle [Fixture] Personen“ → „Vorlage erstellen“. Divi setzt dabei selbst „Globaler Header“ (1061) als Kopfzeile und den Titel „Alle [Fixture] Personen“ (`autogenerated_title` 1); die neue Vorlage ist **aktiv** (`enabled` 1).
2. Hauptinhalt: „Benutzerdefinierten Hauptinhalt erstellen“ → `et_theme_builder_api_create_layout` (`builder/frontend-builder/theme-builder/api.php:12-46`) → 2391, dann `et_theme_builder_api_get_layout_url` (`api.php:116-147`); der Editor öffnet den Visual Builder als Overlay (`iframe#et-fb-app-frame`, `…?et_fb=1&et_tb=1&app_window=1`).
3. Im Visual Builder (Schritt 3), danach „Beenden“.
4. Menü „⋮“ der Vorlage → „Template Deaktivieren“, „Vorlage umbenennen“ → „[Fixture 3b] Person“; Fußzeile leer gelassen.
5. „Änderungen Speichern“ → `et_theme_builder_api_save` mit `live=1` (`api.php:160-273` → `et_theme_builder_store_template`, `builder/frontend-builder/theme-builder/theme-builder.php:1463`). Die neue Vorlage bekam dabei die ID 2394 (die UUID aus dem Editor wurde ersetzt).
### 3. Body-Layout im Visual Builder (2391)
Der leere Abschnitt kam von Divi. Dann über die Oberfläche: grünes „+“ → „Zeile einfügen“ → eine Spalte; Modulsuche „Überschrift“; danach über „Spalte → Elemente → Element hinzufügen“ dreimal „Text“ und einmal „Inhalt posten“ (so heißt `divi/post-content` in der deutschen Modulliste). Je Modul im Einstellungsdialog:
- Überschrift: Knopf „Dynamic Content“ am Feld „Überschrift“ → „Beitrag/Archivtitel“ → „Anwenden“; Meta → Elementbeschriftung „Person Titel“.
- Texte: Knopf „Dynamic Content“ am Feld „Fließtext“ → „Benutzerdefiniertes Feld“ → Auswahl „Benutzerdefiniertes Feld auswählen“ → unter „Standard-Meta-Keys“ `role` bzw. `level` bzw. `mentor` → „Anwenden“; Elementbeschriftung „Person Funktion“/„Person Stufe“/„Person Mentor“. Die Pods-Felder standen in der Liste, eine manuelle Eingabe des Schlüssels war nicht nötig.
- „Inhalt posten“: Elementbeschriftung „Person Beitragsinhalt“.
- Seiteneinstellungen (Zahnrad → „Seiteneinstellungen“) → Titel „[Fixture 3b] Person Body“; „Speichern“ → `POST divi/v1/sync-to-server` mit `post_status: publish` (wie in `stage3/`).
Beim Text „Person Stufe“ wurde zuerst versehentlich „Beitrag/Archivtitel“ gewählt, dann mit „Reset field“ zurückgesetzt und neu gewählt. Deshalb steht dort `builderVersion`/`module` vor `content` – die Schlüsselreihenfolge spiegelt die Bearbeitungsreihenfolge, der Inhalt ist identisch mit den anderen beiden Texten.
### 4. Darstellung (Ruling Q5, Vorlage kurz aktiv)
Im TB-Editor: Bedingung auf genau eine Person eingeschränkt („Bestimmte [Fixture] Personen“ → nur „[Fixture 3b] Erika Muster“, `singular:post_type:lit_fixture_person:id:2385`), „Template Aktivieren“, „Änderungen Speichern“. Ein Wächter im Browser hätte jedes Live-Speichern abgebrochen, in dem eine aktive Vorlage etwas anderes als genau diese Bedingung trägt; ein Notfall-Skript (`_et_enabled` = 0, Bedingung zurück auf „alle“, Cache leeren) lief im `finally` mit. Dann Seiten abgerufen (`?p=2385`, `?p=2386`, Startseite), sofort „Template Deaktivieren“ + Speichern und die Bedingung wieder auf „Alle [Fixture] Personen“ + Speichern. Die Vorlage war etwa zwei Minuten aktiv.
Geprüft: `_et_enabled` von 2394 = `0`; während der aktiven Zeit enthielten Max (2386) und die Startseite keine `_tb_body`/`_tb_header`-Module; danach enthalten weder Erika noch Max, die Startseite noch die Seite 1053 ein TB-Modul.
`render--person-erika.txt` ist der sichtbare Text der fünf Module aus dem HTML von `?p=2385` (je Zeile „Admin-Label: Text“).
### 5. Befund 10 (Schritt 9)
Tab A: TB-Editor frisch geladen (drei Vorlagen). Im Terminal eine deaktivierte Vorlage „[Fixture 3b] Stale-Test“ (2395) so angehängt wie `DiviThemeBuilder::attach()` (Backup-Option, `add_post_meta(1064, '_et_template', 2395)`, Backup löschen) → Liste [1065, 1066, 2394, 2395]. In Tab A „[Fixture 3b] Person“ umbenannt und zurückbenannt, „Änderungen Speichern“. Danach 2395 und die TB-Entwurfsreste (siehe Befund 11) mit `wp post delete --force` gelöscht; `_et_template` = [1065, 1066, 2394].
## Was echt ist und was nachgebildet wurde
**Echt aus Divi (Oberfläche + Server):** Blockstruktur und Attribute aller Module (auch die Standardattribute beim Einfügen, z. B. `columnStructure`/`flexColumnStructure` der Zeile), die Dynamic-Content-Werte mit ihren `settings`, Admin-Labels, Titel, alle Meta des Body-Layouts und der Vorlage, `_et_template` am TB-Post, die Bedingung, der Aktiv-Schalter.
**Nachgebildet bzw. nicht aus Divi:** nur die Zuordnung der Stale-Test-Vorlage in Befund 10 (nachgebaut nach `DiviThemeBuilder::attach()`) und die Pods-Daten (über die Pods-API, nicht die Pods-Oberfläche). Das Notfall-Skript aus Schritt 4 hat nach Divis Speichern noch einmal `_et_use_on` gelöscht und neu gesetzt (gleicher Wert); das spätere Speichern in Befund 10 hat die Vorlage 2394 wieder über `et_theme_builder_store_template` geschrieben, die exportierten Meta sind also Divis eigene.
## Befunde
**Befund 10 – ein alter TB-Editor entfernt fremd angehängte Vorlagen aus der Liste.** Beobachtet: Nach dem Speichern in Tab A stand 2395 **nicht mehr** in `_et_template` (Liste [1065, 1066, 2394]), und 2395 bekam `_et_theme_builder_marked_as_unused` = `2026-09-27 12:47:02`. Divi hätte den Post nach sieben Tagen beim nächsten TB-Speichern in den Papierkorb gelegt. Ursache im Code: `et_theme_builder_api_save()` löscht beim letzten Teil-Request die komplette Zuordnung (`api.php:233`, `delete_post_meta( $theme_builder_id, '_et_template' )`) und schreibt nur die Vorlagen, die der Editor mitschickt (`api.php:238-240`); der Editor schickt seinen beim Laden übernommenen Stand (`et_theme_builder_bundle.config.templates.live`). Bei `live=1` folgt `et_theme_builder_trash_draft_and_unused_posts()` (`api.php:245-247` → `theme-builder.php:591-678`): alle TB-Vorlagen und -Layouts, die die neue Liste nicht referenziert, werden markiert (`theme-builder.php:646`), markierte Posts älter als sieben Tage in den Papierkorb gelegt (`theme-builder.php:651-676`). Der Editor zeigt die fremde Vorlage nicht an und warnt nicht. Folge für das Plugin: Jede über MCP angehängte Vorlage (und ihre Layouts) geht verloren, sobald jemand einen vorher geöffneten TB-Editor speichert – Divi hebt die Markierung nur wieder auf, wenn die Vorlage erneut in der Liste steht (`theme-builder.php:620-623`).
**Befund 11 – der TB-Entwurf entsteht erst durch Divis Autosave.** Öffnen des Editors: kein Entwurf. Erste ungespeicherte Änderung (neue Vorlage): sofort noch kein Entwurf. Divi speichert Entwürfe per Intervall alle 120 s, wenn es ungespeicherte Änderungen gibt (`theme-builder.js`, Suche `e.autosave=setInterval`, `12e4`, `save({draft:!0})` → `et_theme_builder_api_save` mit `live=0`). Beim ersten Tick entstand der Entwurfs-TB-Post 2387 (`auto-draft`) mit **Kopien aller Vorlagen** als eigene `et_template`-Posts (2388–2390, Status `publish`, die Kopie der neuen Vorlage mit `enabled` 1). Weitere Ticks aktualisierten dieselben Posts. Das Live-Speichern legte 2387 in den Papierkorb (`theme-builder.php:604`) und markierte 2388–2390 als ungenutzt. Das neue Body-Layout 2391 entstand unabhängig davon beim Klick „Benutzerdefinierten Hauptinhalt erstellen“, also schon vor jedem Speichern. Solange ein Entwurf besteht, zeigt der Editor beim nächsten Öffnen dessen Stand (`config.templates.hasDraft`).
**Befund 12 – Form der Pods-Felder im Body.** Der echte Dialog schreibt für ein aus der Liste gewähltes Pods-Feld **weder** `custom_meta_role` als Namen **noch** `post_meta_key` mit `meta_key` `role`, sondern:
```
$variable({"type":"content","value":{"name":"post_meta_key","settings":{"before":"","after":"","select_meta_key":"custom_meta_role","meta_key":"","date_format":"default","custom_date_format":"","enable_html":"off"}}})$
```
(in `post_content` steht `\u0022` statt `"`, `tb-body--person-body.html`). Gleiches für `level`/`mentor`. `meta_key` ist leer, `date_format`/`custom_date_format`/`enable_html` werden immer mitgeschrieben. Der Beitragstitel hat nur `{"before":"","after":""}`. Divi nennt diese Form im Server „Legacy complex format“ und löst sie über `select_meta_key` auf (`builder-5/server/Packages/Module/Layout/Components/DynamicContent/DynamicContentOptionPostMetaKey.php:236-262` bzw. `:278-296`); `custom_meta_<key>` als Name heißt dort „Simple format (preferred)“ (`:225-235`). Ein aus der Liste gewähltes Feld gilt nicht als manuelle Eingabe (`DynamicContentUtils.php:527-537`) und braucht im Builder das Recht `read_dynamic_content_custom_fields` (`DynamicContentOptionPostMetaKey.php:305-316`), im Frontend nicht. Die Liste „Standard-Meta-Keys“ stammt aus `DynamicContentOptions::get_most_used_meta_keys()` (`DynamicContentOptions.php:286-316`): Meta-Schlüssel ohne führendes `_` mit nicht serialisierten Werten aus öffentlichen Post-Typen der ganzen Site, 5 Minuten zwischengespeichert – nicht die Felder des Pods; `_pods_mentor` (serialisiert) fehlt deshalb, `mentor` steht drin. Das Plugin schreibt selbst die Namensform `custom_meta_<key>` (`src/Integrations/Divi/Compose/Placeholders.php:196`) und prüft `select_meta_key`-Werte bereits (`src/Integrations/Divi/Compose/DynamicContentRules.php:175-180`). Wer im Body nach „dem Feld role“ sucht oder es ändert, muss beide Formen erkennen.
Ausgabe laut `render--person-erika.txt`: `role` → „Geschäftsführung“; `level` → **`senior`** (gespeicherter Wert, nicht die Beschriftung „Senior“); `mentor` bei Erika leer (sie hat keinen Mentor; Divi gibt im Frontend bei leerem Wert nichts aus, `DynamicContentOptionPostMetaKey.php:433`). Für Max wurde die Vorlage nicht aktiviert (Q5: genau eine Person); Divis Resolver liefert dort nur lesend aufgerufen (`DynamicContentUtils::get_resolved_value`, Kontext `display`) für `mentor` **`2385`** – die rohe Beitrags-ID, keinen Titel (F26).
## Wirkung auf die Live-Site
Die Vorlage 2394 ist deaktiviert (`_et_enabled` = 0); `ET_Theme_Builder_Request::get_template()` überspringt sie (`builder/frontend-builder/theme-builder/ThemeBuilderRequest.php:393`). Sie wäre zudem nur für Fixture-Personen zuständig. Die Standardvorlage 1065 und 1066 blieben deaktiviert. Stale-Test 2395 und die Entwurfsreste 2387–2390 sind gelöscht, kein Post ist als ungenutzt markiert, es gibt keinen TB-Entwurf. Die Divi-Einstellung „Beitragstyp-Integration“ ist unverändert. Neu und sichtbar sind nur die zwei öffentlichen Fixture-Personen unter `/lit-fixture-person/…` und der Menüpunkt „[Fixture] Personen“ im Backend.
## Neu exportieren
```
ddev wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage3-fixtures.php stage3b "[Fixture 3b]"
```
Exportiert werden Posts mit Titel „[Fixture 3b] …“ (auch vom Typ `lit_fixture_person`). Das Skript löscht vorher alle `*.json`/`*.html` in diesem Ordner – `render--person-erika.txt` und diese README bleiben. Ohne Argumente exportiert es wie bisher `stage3/`; dessen `theme-builder.json` enthält seit diesen Fixtures auch 2394 (siehe `stage3/README.md`).
## Aufräumen (lokal)
Die Fixtures bleiben stehen. Zum Entfernen (DDEV, WordPress-Wurzel; IDs vorher mit `manifest.json` abgleichen):
```
# Vorlage aus der Live-Liste nehmen, dann Vorlage und Body-Layout (Revisionen fallen mit weg)
ddev wp eval 'delete_post_meta(1064, "_et_template", "2394");'
ddev wp post delete 2394 2391 --force
# Personen und Pod-Definition (Pod 2381, Felder 2382–2384)
ddev wp post delete 2385 2386 --force
ddev wp eval 'pods_api()->delete_pod(["name" => "lit_fixture_person"], false, true);'
# Divi-Caches
ddev wp eval 'et_theme_builder_clear_wp_cache("all"); ET_Core_PageResource::remove_static_resources("all", "all");'
```
Nicht mit dem TB-Editor aufräumen, solange die `stage3/`-Fixtures noch gebraucht werden: Speichern im Editor schreibt die ganze Liste neu (Befund 10).
@@ -0,0 +1 @@
<p>Beitragsinhalt der Fixture-Person Erika Muster.</p>
@@ -0,0 +1,26 @@
{
"post": {
"ID": 2385,
"post_type": "lit_fixture_person",
"post_status": "publish",
"post_title": "[Fixture 3b] Erika Muster",
"post_name": "fixture-3b-erika-muster",
"post_author": 0,
"post_parent": 0,
"menu_order": 0,
"post_excerpt": "",
"post_date_gmt": "2026-09-27 12:27:46",
"post_modified_gmt": "2026-09-27 12:27:46",
"content_file": "lit_fixture_person--erika-muster.html",
"content_sha256": "1ba6ea6d3a4ca9904d75150f03820fbe3df63123159f661fe00630f0f75d3eeb"
},
"meta": {
"role": [
"Geschäftsführung"
],
"level": [
"senior"
]
},
"terms": {}
}
@@ -0,0 +1 @@
<p>Beitragsinhalt der Fixture-Person Max Beispiel.</p>
@@ -0,0 +1,32 @@
{
"post": {
"ID": 2386,
"post_type": "lit_fixture_person",
"post_status": "publish",
"post_title": "[Fixture 3b] Max Beispiel",
"post_name": "fixture-3b-max-beispiel",
"post_author": 0,
"post_parent": 0,
"menu_order": 0,
"post_excerpt": "",
"post_date_gmt": "2026-09-27 12:27:46",
"post_modified_gmt": "2026-09-27 12:27:46",
"content_file": "lit_fixture_person--max-beispiel.html",
"content_sha256": "9724e93063332c903dcc04e82b9a9b956da5b61f0d3321ca5e7b0886164090da"
},
"meta": {
"role": [
"Entwicklung"
],
"level": [
"junior"
],
"_pods_mentor": [
"a:1:{i:0;i:2385;}"
],
"mentor": [
"2385"
]
},
"terms": {}
}
@@ -0,0 +1,9 @@
{
"divi_version": "5.2.1",
"posts": {
"lit_fixture_person--erika-muster": 2385,
"lit_fixture_person--max-beispiel": 2386,
"tb-body--person-body": 2391,
"tb-template--person": 2394
}
}
@@ -0,0 +1,5 @@
Person Titel: [Fixture 3b] Erika Muster
Person Funktion: Geschäftsführung
Person Stufe: senior
Person Mentor:
Person Beitragsinhalt: Beitragsinhalt der Fixture-Person Erika Muster.
@@ -0,0 +1,15 @@
<!-- wp:divi/placeholder --><!-- wp:divi/section {"builderVersion":"5.2.1"} -->
<!-- wp:divi/row {"module":{"advanced":{"columnStructure":{"desktop":{"value":"4_4"}},"flexColumnStructure":{"desktop":{"value":"equal-columns_1"}}},"decoration":{"layout":{"desktop":{"value":{"flexWrap":"nowrap"}}}}},"builderVersion":"5.2.1"} -->
<!-- wp:divi/column {"module":{"advanced":{"type":{"desktop":{"value":"4_4"}}},"decoration":{"sizing":{"desktop":{"value":{"flexType":"24_24"}}}}},"builderVersion":"5.2.1"} -->
<!-- wp:divi/heading {"module":{"meta":{"adminLabel":{"desktop":{"value":"Person Titel"}}}},"title":{"innerContent":{"desktop":{"value":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022post_title\u0022,\u0022settings\u0022:{\u0022before\u0022:\u0022\u0022,\u0022after\u0022:\u0022\u0022}}})$"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/text {"module":{"meta":{"adminLabel":{"desktop":{"value":"Person Funktion"}}}},"content":{"innerContent":{"desktop":{"value":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022post_meta_key\u0022,\u0022settings\u0022:{\u0022before\u0022:\u0022\u0022,\u0022after\u0022:\u0022\u0022,\u0022select_meta_key\u0022:\u0022custom_meta_role\u0022,\u0022meta_key\u0022:\u0022\u0022,\u0022date_format\u0022:\u0022default\u0022,\u0022custom_date_format\u0022:\u0022\u0022,\u0022enable_html\u0022:\u0022off\u0022}}})$"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/text {"module":{"meta":{"adminLabel":{"desktop":{"value":"Person Stufe"}}}},"content":{"innerContent":{"desktop":{"value":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022post_meta_key\u0022,\u0022settings\u0022:{\u0022before\u0022:\u0022\u0022,\u0022after\u0022:\u0022\u0022,\u0022select_meta_key\u0022:\u0022custom_meta_level\u0022,\u0022meta_key\u0022:\u0022\u0022,\u0022date_format\u0022:\u0022default\u0022,\u0022custom_date_format\u0022:\u0022\u0022,\u0022enable_html\u0022:\u0022off\u0022}}})$"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/text {"module":{"meta":{"adminLabel":{"desktop":{"value":"Person Mentor"}}}},"content":{"innerContent":{"desktop":{"value":"$variable({\u0022type\u0022:\u0022content\u0022,\u0022value\u0022:{\u0022name\u0022:\u0022post_meta_key\u0022,\u0022settings\u0022:{\u0022before\u0022:\u0022\u0022,\u0022after\u0022:\u0022\u0022,\u0022select_meta_key\u0022:\u0022custom_meta_mentor\u0022,\u0022meta_key\u0022:\u0022\u0022,\u0022date_format\u0022:\u0022default\u0022,\u0022custom_date_format\u0022:\u0022\u0022,\u0022enable_html\u0022:\u0022off\u0022}}})$"}}},"builderVersion":"5.2.1"} /-->
<!-- wp:divi/post-content {"module":{"meta":{"adminLabel":{"desktop":{"value":"Person Beitragsinhalt"}}}},"builderVersion":"5.2.1"} /-->
<!-- /wp:divi/column -->
<!-- /wp:divi/row -->
<!-- /wp:divi/section --><!-- /wp:divi/placeholder -->
@@ -0,0 +1,71 @@
{
"post": {
"ID": 2391,
"post_type": "et_body_layout",
"post_status": "publish",
"post_title": "[Fixture 3b] Person Body",
"post_name": "theme-builder-layout-2",
"post_author": 1,
"post_parent": 0,
"menu_order": 0,
"post_excerpt": "",
"post_date_gmt": "2026-09-27 12:31:20",
"post_modified_gmt": "2026-09-27 12:40:46",
"content_file": "tb-body--person-body.html",
"content_sha256": "14037fe5c0e032c19ab9015272464d1dcd887bf08630972ef05dc20aae04e203"
},
"meta": {
"_et_pb_use_builder": [
"on"
],
"_et_pb_show_page_creation": [
"off"
],
"_et_pb_ab_current_shortcode": [
"[et_pb_split_track id=\"2391\" /]"
],
"_et_pb_content_area_background_color": [
""
],
"_et_pb_custom_css": [
""
],
"_et_pb_enable_shortcode_tracking": [
"off"
],
"_et_pb_gutter_width": [
"3"
],
"_et_pb_page_z_index": [
"auto"
],
"_thumbnail_id": [
""
],
"_et_pb_section_background_color": [
""
],
"_et_pb_post_settings_title": [
"[Fixture 3b] Person Body"
],
"_et_pb_post_settings_excerpt": [
""
],
"_et_pb_first_image": [
""
],
"_et_pb_truncate_post": [
""
],
"_et_pb_truncate_post_date": [
""
],
"_divi_off_canvas_data": [
"a:2:{s:14:\"activeCanvasId\";s:36:\"0e37b4ab-8f7e-488f-b9cf-7ae508b71ad6\";s:14:\"mainCanvasName\";s:11:\"Main Canvas\";}"
],
"_et_pb_use_divi_5": [
"on"
]
},
"terms": {}
}
@@ -0,0 +1,53 @@
{
"post": {
"ID": 2394,
"post_type": "et_template",
"post_status": "publish",
"post_title": "[Fixture 3b] Person",
"post_name": "fixture-3b-person",
"post_author": 1,
"post_parent": 0,
"menu_order": 0,
"post_excerpt": "",
"post_date_gmt": "2026-09-27 12:42:26",
"post_modified_gmt": "2026-09-27 12:42:26",
"content_file": null,
"content_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"meta": {
"_et_autogenerated_title": [
"0"
],
"_et_default": [
"0"
],
"_et_enabled": [
"0"
],
"_et_header_layout_id": [
"1061"
],
"_et_header_layout_enabled": [
"1"
],
"_et_body_layout_id": [
"2391"
],
"_et_body_layout_enabled": [
"1"
],
"_et_footer_layout_id": [
"0"
],
"_et_footer_layout_enabled": [
"1"
],
"_et_onboarding_created": [
"0"
],
"_et_use_on": [
"singular:post_type:lit_fixture_person:all"
]
},
"terms": {}
}
@@ -0,0 +1,22 @@
{
"theme_builder_post": {
"ID": 1064,
"post_status": "publish",
"post_title": "Theme Builder",
"meta": {
"_et_template": [
"1065",
"1066",
"2394"
]
}
},
"template_ids_in_order": [
1065,
1066,
2394
],
"draft_theme_builder_posts": [],
"options_et_tb": {},
"marked_as_unused": []
}
@@ -9,21 +9,24 @@
* *
* Usage (DDEV, WordPress root): * Usage (DDEV, WordPress root):
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage3-fixtures.php * wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage3-fixtures.php
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage3-fixtures.php stage3b "[Fixture 3b]"
* Arguments: target folder below tests/Fixtures/divi (default stage3) and title prefix (default "[Fixture]").
* *
* Only posts titled "[Fixture] …" are exported, plus the Default Website Template when its * Only posts titled "<prefix> …" are exported, plus the Default Website Template when its
* layouts are fixture layouts. Read-only. Fixtures are committed – no personal data. * layouts are fixture layouts. Read-only. Fixtures are committed – no personal data.
* No `declare(strict_types=1);` – wp eval-file runs the file through eval(). * No `declare(strict_types=1);` – wp eval-file runs the file through eval().
*/ */
defined('WP_CLI') && WP_CLI || exit; defined('WP_CLI') && WP_CLI || exit;
$lit_prefix = '[Fixture] '; $lit_stage = preg_match('/^[a-z0-9]+$/D', (string) ($args[0] ?? '')) === 1 ? (string) $args[0] : 'stage3';
$lit_dir = dirname(__DIR__) . '/Fixtures/divi/stage3'; $lit_prefix = (string) ($args[1] ?? '[Fixture]') . ' ';
$lit_dir = dirname(__DIR__) . '/Fixtures/divi/' . $lit_stage;
$lit_skip_meta = ['_edit_lock', '_edit_last']; $lit_skip_meta = ['_edit_lock', '_edit_last'];
/** Role name for a fixture post, derived from post type, title and scope. */ /** Role name for a fixture post, derived from post type, title and scope. */
$lit_role = static function (WP_Post $post, array $terms): string { $lit_role = static function (WP_Post $post, array $terms): string {
$slug = sanitize_title(preg_replace('/^\[Fixture\]\s*/', '', $post->post_title)); $slug = sanitize_title(preg_replace('/^\[Fixture[^\]]*\]\s*/', '', $post->post_title));
$prefix = [ $prefix = [
'page' => 'page', 'page' => 'page',
'et_pb_layout' => in_array('global', array_column($terms['scope'] ?? [], 'slug'), true) 'et_pb_layout' => in_array('global', array_column($terms['scope'] ?? [], 'slug'), true)
@@ -33,6 +36,7 @@ $lit_role = static function (WP_Post $post, array $terms): string {
'et_header_layout' => 'tb-header', 'et_header_layout' => 'tb-header',
'et_body_layout' => 'tb-body', 'et_body_layout' => 'tb-body',
'et_footer_layout' => 'tb-footer', 'et_footer_layout' => 'tb-footer',
'lit_fixture_person' => 'lit_fixture_person',
][$post->post_type] ?? $post->post_type; ][$post->post_type] ?? $post->post_type;
return $prefix . '--' . $slug; return $prefix . '--' . $slug;
@@ -85,7 +89,7 @@ $lit_json = static fn (array $data): string => wp_json_encode(
) . "\n"; ) . "\n";
global $wpdb; global $wpdb;
$lit_types = ['page', 'et_pb_layout', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout']; $lit_types = ['page', 'et_pb_layout', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout', 'lit_fixture_person'];
$lit_ids = array_map('intval', $wpdb->get_col($wpdb->prepare( $lit_ids = array_map('intval', $wpdb->get_col($wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} "SELECT ID FROM {$wpdb->posts}
WHERE post_type IN (" . implode(', ', array_fill(0, count($lit_types), '%s')) . ") WHERE post_type IN (" . implode(', ', array_fill(0, count($lit_types), '%s')) . ")
@@ -113,7 +117,11 @@ foreach ($lit_tb_templates as $templateId) {
} }
if ($lit_ids === []) { if ($lit_ids === []) {
WP_CLI::error('No "[Fixture] " posts found – create them first (see tests/Fixtures/divi/stage3/README.md).'); WP_CLI::error(sprintf(
'No "%s" posts found – create them first (see tests/Fixtures/divi/%s/README.md).',
$lit_prefix,
$lit_stage
));
} }
if (! is_dir($lit_dir) && ! wp_mkdir_p($lit_dir)) { if (! is_dir($lit_dir) && ! wp_mkdir_p($lit_dir)) {
+71 -1
View File
@@ -1,11 +1,15 @@
<?php <?php
/** /**
* Creates or removes the Pods test content type "lit_person" for integration tests. * Creates or removes the Pods test content type "lit_person" for integration tests, or creates the
* permanent Etappe 3b fixture pod "lit_fixture_person" with two persons (tests/Fixtures/divi/stage3b).
* *
* Usage (inside DDEV, Pods must be active): * Usage (inside DDEV, Pods must be active):
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-fixture.php create * wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-fixture.php create
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-fixture.php delete * wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-fixture.php delete
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/pods-fixture.php fixture
*
* "delete" only removes "lit_person"; the fixture pod is kept (smokes never touch it).
* *
* No strict_types: wp eval-file runs this through eval(). * No strict_types: wp eval-file runs this through eval().
*/ */
@@ -28,6 +32,72 @@ if ($action === 'delete') {
return; return;
} }
if ($action === 'fixture') {
// Etappe 3b fixture pod (ruling Q1): its own type, so no smoke deletes the fixture persons.
if (! $api->load_pod(['name' => 'lit_fixture_person'])) {
$api->save_pod([
'name' => 'lit_fixture_person',
'label' => '[Fixture] Personen',
'label_singular' => '[Fixture] Person',
'type' => 'post_type',
'storage' => 'meta',
'public' => 1,
'show_ui' => 1,
]);
}
$fixtureFields = [
['name' => 'role', 'label' => 'Funktion', 'type' => 'text'],
[
'name' => 'level',
'label' => 'Stufe',
'type' => 'pick',
'pick_object' => 'custom-simple',
'pick_custom' => "junior|Junior\nsenior|Senior",
],
[
'name' => 'mentor',
'label' => 'Mentor',
'type' => 'pick',
'pick_object' => 'post_type',
'pick_val' => 'lit_fixture_person',
'pick_format_type' => 'single',
],
];
foreach ($fixtureFields as $field) {
if (! $api->load_field(['pod' => 'lit_fixture_person', 'name' => $field['name']])) {
$api->save_field(array_merge(['pod' => 'lit_fixture_person'], $field));
}
}
$existing = static function (string $title): int {
$found = get_posts([
'post_type' => 'lit_fixture_person',
'post_status' => 'any',
'title' => $title,
'fields' => 'ids',
'numberposts' => 1,
]);
return (int) ($found[0] ?? 0);
};
$erika = $existing('[Fixture 3b] Erika Muster') ?: (int) pods('lit_fixture_person')->add([
'post_title' => '[Fixture 3b] Erika Muster',
'post_content' => '<p>Beitragsinhalt der Fixture-Person Erika Muster.</p>',
'post_status' => 'publish',
'role' => 'Geschäftsführung',
'level' => 'senior',
]);
$max = $existing('[Fixture 3b] Max Beispiel') ?: (int) pods('lit_fixture_person')->add([
'post_title' => '[Fixture 3b] Max Beispiel',
'post_content' => '<p>Beitragsinhalt der Fixture-Person Max Beispiel.</p>',
'post_status' => 'publish',
'role' => 'Entwicklung',
'level' => 'junior',
'mentor' => $erika,
]);
echo "persons {$erika} {$max}\n";
return;
}
if (! $api->load_pod(['name' => 'lit_person'])) { if (! $api->load_pod(['name' => 'lit_person'])) {
$api->save_pod([ $api->save_pod([
'name' => 'lit_person', 'name' => 'lit_person',
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Integrations\Divi\BlockTree;
use LIT\MCP\Integrations\Divi\BlockTree\Parser;
use LIT\MCP\Integrations\Divi\BlockTree\Serializer;
use LIT\MCP\Tests\Support\DiviFixtures;
use PHPUnit\Framework\TestCase;
/** The Etappe 3b builder fixtures (tests/Fixtures/divi/stage3b, README): real Divi data with Pods fields. */
final class Stage3bFixturesTest extends TestCase
{
/** @return array<string, array{string}> */
public static function files(): array
{
$files = glob(dirname(__DIR__, 4) . '/Fixtures/divi/stage3b/*.html') ?: [];
$cases = [];
foreach ($files as $file) {
$cases[basename($file)] = ['stage3b/' . basename($file)];
}
return $cases;
}
public function testTheFixturesExist(): void
{
self::assertArrayHasKey('tb-body--person-body.html', self::files());
self::assertFileExists(dirname(__DIR__, 4) . '/Fixtures/divi/stage3b/tb-template--person.json');
}
/** @dataProvider files */
public function testParsesAndSerializesByteIdentically(string $name): void
{
$content = DiviFixtures::markup($name);
self::assertSame($content, Serializer::document((new Parser())->parse($content)));
}
/** Finding 12: the Pods field "role" as Divi's dialog writes it (custom_meta_role or post_meta_key + meta_key). */
public function testBodyLayoutShowsThePodsFieldRole(): void
{
$body = DiviFixtures::markup('stage3b/tb-body--person-body.html');
self::assertTrue(
str_contains($body, 'custom_meta_role') || str_contains($body, 'meta_key":"role"'),
'the body layout references the Pods field role'
);
self::assertStringContainsString('wp:divi/post-content', $body);
}
public function testTheTemplateIsDisabledAndMeantForPersons(): void
{
$template = json_decode(DiviFixtures::markup('stage3b/tb-template--person.json'), true);
self::assertSame(['0'], $template['meta']['_et_enabled']);
self::assertSame(['singular:post_type:lit_fixture_person:all'], $template['meta']['_et_use_on']);
self::assertSame(['1061'], $template['meta']['_et_header_layout_id'], 'global header of the default template');
}
}