From c615522cf690007ad7a790e05d8bfeb50d557f63 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Sun, 27 Sep 2026 14:51:19 +0200 Subject: [PATCH] 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) --- tests/Fixtures/divi/stage3/README.md | 2 + tests/Fixtures/divi/stage3b/README.md | 111 ++++++++++++++++++ .../lit_fixture_person--erika-muster.html | 1 + .../lit_fixture_person--erika-muster.json | 26 ++++ .../lit_fixture_person--max-beispiel.html | 1 + .../lit_fixture_person--max-beispiel.json | 32 +++++ tests/Fixtures/divi/stage3b/manifest.json | 9 ++ .../divi/stage3b/render--person-erika.txt | 5 + .../divi/stage3b/tb-body--person-body.html | 15 +++ .../divi/stage3b/tb-body--person-body.json | 71 +++++++++++ .../divi/stage3b/tb-template--person.json | 53 +++++++++ .../Fixtures/divi/stage3b/theme-builder.json | 22 ++++ .../divi-export-stage3-fixtures.php | 20 +++- tests/Integration/pods-fixture.php | 72 +++++++++++- .../Divi/BlockTree/Stage3bFixturesTest.php | 61 ++++++++++ 15 files changed, 494 insertions(+), 7 deletions(-) create mode 100644 tests/Fixtures/divi/stage3b/README.md create mode 100644 tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.html create mode 100644 tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.json create mode 100644 tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.html create mode 100644 tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.json create mode 100644 tests/Fixtures/divi/stage3b/manifest.json create mode 100644 tests/Fixtures/divi/stage3b/render--person-erika.txt create mode 100644 tests/Fixtures/divi/stage3b/tb-body--person-body.html create mode 100644 tests/Fixtures/divi/stage3b/tb-body--person-body.json create mode 100644 tests/Fixtures/divi/stage3b/tb-template--person.json create mode 100644 tests/Fixtures/divi/stage3b/theme-builder.json create mode 100644 tests/Unit/Integrations/Divi/BlockTree/Stage3bFixturesTest.php diff --git a/tests/Fixtures/divi/stage3/README.md b/tests/Fixtures/divi/stage3/README.md index e81f8f1..2c93140 100644 --- a/tests/Fixtures/divi/stage3/README.md +++ b/tests/Fixtures/divi/stage3/README.md @@ -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. +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) Die Daten bleiben vorerst zum Ansehen stehen. Zum Entfernen (DDEV, WordPress-Wurzel; IDs vorher mit `manifest.json` abgleichen): diff --git a/tests/Fixtures/divi/stage3b/README.md b/tests/Fixtures/divi/stage3b/README.md new file mode 100644 index 0000000..ecc34cb --- /dev/null +++ b/tests/Fixtures/divi/stage3b/README.md @@ -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 `.json` (Post-Zeile, alle Meta roh in Speicherreihenfolge, Terme) und `.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_` 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_` (`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). diff --git a/tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.html b/tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.html new file mode 100644 index 0000000..e5b4788 --- /dev/null +++ b/tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.html @@ -0,0 +1 @@ +

Beitragsinhalt der Fixture-Person Erika Muster.

\ No newline at end of file diff --git a/tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.json b/tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.json new file mode 100644 index 0000000..0802091 --- /dev/null +++ b/tests/Fixtures/divi/stage3b/lit_fixture_person--erika-muster.json @@ -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": {} +} diff --git a/tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.html b/tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.html new file mode 100644 index 0000000..0ac547d --- /dev/null +++ b/tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.html @@ -0,0 +1 @@ +

Beitragsinhalt der Fixture-Person Max Beispiel.

\ No newline at end of file diff --git a/tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.json b/tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.json new file mode 100644 index 0000000..63c9ceb --- /dev/null +++ b/tests/Fixtures/divi/stage3b/lit_fixture_person--max-beispiel.json @@ -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": {} +} diff --git a/tests/Fixtures/divi/stage3b/manifest.json b/tests/Fixtures/divi/stage3b/manifest.json new file mode 100644 index 0000000..406497d --- /dev/null +++ b/tests/Fixtures/divi/stage3b/manifest.json @@ -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 + } +} diff --git a/tests/Fixtures/divi/stage3b/render--person-erika.txt b/tests/Fixtures/divi/stage3b/render--person-erika.txt new file mode 100644 index 0000000..d1b0475 --- /dev/null +++ b/tests/Fixtures/divi/stage3b/render--person-erika.txt @@ -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. diff --git a/tests/Fixtures/divi/stage3b/tb-body--person-body.html b/tests/Fixtures/divi/stage3b/tb-body--person-body.html new file mode 100644 index 0000000..ec525df --- /dev/null +++ b/tests/Fixtures/divi/stage3b/tb-body--person-body.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Fixtures/divi/stage3b/tb-body--person-body.json b/tests/Fixtures/divi/stage3b/tb-body--person-body.json new file mode 100644 index 0000000..f2f01d8 --- /dev/null +++ b/tests/Fixtures/divi/stage3b/tb-body--person-body.json @@ -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": {} +} diff --git a/tests/Fixtures/divi/stage3b/tb-template--person.json b/tests/Fixtures/divi/stage3b/tb-template--person.json new file mode 100644 index 0000000..ebcfdcf --- /dev/null +++ b/tests/Fixtures/divi/stage3b/tb-template--person.json @@ -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": {} +} diff --git a/tests/Fixtures/divi/stage3b/theme-builder.json b/tests/Fixtures/divi/stage3b/theme-builder.json new file mode 100644 index 0000000..d3aa8a1 --- /dev/null +++ b/tests/Fixtures/divi/stage3b/theme-builder.json @@ -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": [] +} diff --git a/tests/Integration/divi-export-stage3-fixtures.php b/tests/Integration/divi-export-stage3-fixtures.php index c4b2939..1c48e47 100644 --- a/tests/Integration/divi-export-stage3-fixtures.php +++ b/tests/Integration/divi-export-stage3-fixtures.php @@ -9,21 +9,24 @@ * * 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 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 " …" are exported, plus the Default Website Template when its * 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(). */ defined('WP_CLI') && WP_CLI || exit; -$lit_prefix = '[Fixture] '; -$lit_dir = dirname(__DIR__) . '/Fixtures/divi/stage3'; +$lit_stage = preg_match('/^[a-z0-9]+$/D', (string) ($args[0] ?? '')) === 1 ? (string) $args[0] : 'stage3'; +$lit_prefix = (string) ($args[1] ?? '[Fixture]') . ' '; +$lit_dir = dirname(__DIR__) . '/Fixtures/divi/' . $lit_stage; $lit_skip_meta = ['_edit_lock', '_edit_last']; /** Role name for a fixture post, derived from post type, title and scope. */ $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 = [ 'page' => 'page', '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_body_layout' => 'tb-body', 'et_footer_layout' => 'tb-footer', + 'lit_fixture_person' => 'lit_fixture_person', ][$post->post_type] ?? $post->post_type; return $prefix . '--' . $slug; @@ -85,7 +89,7 @@ $lit_json = static fn (array $data): string => wp_json_encode( ) . "\n"; 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( "SELECT ID FROM {$wpdb->posts} 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 === []) { - 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)) { diff --git a/tests/Integration/pods-fixture.php b/tests/Integration/pods-fixture.php index b5a9b5c..397db68 100644 --- a/tests/Integration/pods-fixture.php +++ b/tests/Integration/pods-fixture.php @@ -1,11 +1,15 @@ 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' => '

Beitragsinhalt der Fixture-Person Erika Muster.

', + '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' => '

Beitragsinhalt der Fixture-Person Max Beispiel.

', + 'post_status' => 'publish', + 'role' => 'Entwicklung', + 'level' => 'junior', + 'mentor' => $erika, + ]); + echo "persons {$erika} {$max}\n"; + return; +} + if (! $api->load_pod(['name' => 'lit_person'])) { $api->save_pod([ 'name' => 'lit_person', diff --git a/tests/Unit/Integrations/Divi/BlockTree/Stage3bFixturesTest.php b/tests/Unit/Integrations/Divi/BlockTree/Stage3bFixturesTest.php new file mode 100644 index 0000000..e536f1f --- /dev/null +++ b/tests/Unit/Integrations/Divi/BlockTree/Stage3bFixturesTest.php @@ -0,0 +1,61 @@ + */ + 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'); + } +}