test: real Divi 5 stage 3 fixtures (library, global instances, Theme Builder)

Created through Divi's own save paths (Visual Builder add-to-library dialog,
divi/v1/divi-library/create-item, divi/v1/sync-to-server, Theme Builder
admin-ajax create_layout/save) and exported by divi-export-stage3-fixtures.php.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 23:05:02 +02:00
co-authored by Claude Opus 5.5
parent 2c553d2f0b
commit e7cc4efb11
26 changed files with 1117 additions and 0 deletions
@@ -0,0 +1,185 @@
<?php
/**
* Exports the stage 3 Divi fixtures (library items, global instances, Theme Builder) from the
* local site into tests/Fixtures/divi/stage3/: one <role>.json per post (post row, all meta,
* all term relationships), the raw post_content byte-exact as <role>.html, the live Theme
* Builder post with its template list and the et_tb_* options as theme-builder.json, and a
* manifest.json (role → post ID). How the fixtures were created: tests/Fixtures/divi/stage3/README.md.
*
* Usage (DDEV, WordPress root):
* wp eval-file wp-content/plugins/WP-LIT-MCP/tests/Integration/divi-export-stage3-fixtures.php
*
* Only posts titled "[Fixture] …" 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_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));
$prefix = [
'page' => 'page',
'et_pb_layout' => in_array('global', array_column($terms['scope'] ?? [], 'slug'), true)
? 'library-global'
: 'library',
'et_template' => 'tb-template',
'et_header_layout' => 'tb-header',
'et_body_layout' => 'tb-body',
'et_footer_layout' => 'tb-footer',
][$post->post_type] ?? $post->post_type;
return $prefix . '--' . $slug;
};
/** All term relationships by taxonomy – read from the tables, some Divi taxonomies are not registered under WP-CLI. */
$lit_terms = static function (int $postId): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT tt.taxonomy, t.slug, t.name FROM {$wpdb->term_relationships} tr
JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
WHERE tr.object_id = %d ORDER BY tt.taxonomy, t.slug",
$postId
));
$terms = [];
foreach ($rows as $row) {
$terms[$row->taxonomy][] = ['slug' => $row->slug, 'name' => $row->name];
}
return $terms;
};
/** Raw meta rows (value as stored, multiple values kept in order). */
$lit_meta = static function (int $postId) use ($lit_skip_meta): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d ORDER BY meta_id",
$postId
));
$meta = [];
foreach ($rows as $row) {
if (! in_array($row->meta_key, $lit_skip_meta, true)) {
$meta[$row->meta_key][] = $row->meta_value;
}
}
return $meta;
};
$lit_write = static function (string $file, string $data): void {
if (file_put_contents($file, $data) === false) {
WP_CLI::error('Cannot write ' . $file);
}
};
$lit_json = static fn (array $data): string => wp_json_encode(
$data,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
) . "\n";
global $wpdb;
$lit_types = ['page', 'et_pb_layout', 'et_template', 'et_header_layout', 'et_body_layout', 'et_footer_layout'];
$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')) . ")
AND post_status NOT IN ('inherit', 'auto-draft')
AND post_title LIKE %s
ORDER BY ID",
...[...$lit_types, $wpdb->esc_like($lit_prefix) . '%']
)));
// The live Theme Builder post and the Default Website Template, when it points to fixture layouts.
$lit_tb_id = (int) $wpdb->get_var(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'et_theme_builder' AND post_status = 'publish' ORDER BY ID LIMIT 1"
);
$lit_tb_templates = $lit_tb_id ? array_map('intval', get_post_meta($lit_tb_id, '_et_template', false)) : [];
foreach ($lit_tb_templates as $templateId) {
if (get_post_meta($templateId, '_et_default', true) !== '1') {
continue;
}
foreach (['header', 'body', 'footer'] as $area) {
$layoutId = (int) get_post_meta($templateId, "_et_{$area}_layout_id", true);
if ($layoutId > 0 && in_array($layoutId, $lit_ids, true) && ! in_array($templateId, $lit_ids, true)) {
$lit_ids[] = $templateId;
}
}
}
if ($lit_ids === []) {
WP_CLI::error('No "[Fixture] " posts found – create them first (see tests/Fixtures/divi/stage3/README.md).');
}
if (! is_dir($lit_dir) && ! wp_mkdir_p($lit_dir)) {
WP_CLI::error('Cannot create ' . $lit_dir);
}
foreach (glob($lit_dir . '/*.{json,html}', GLOB_BRACE) ?: [] as $old) {
unlink($old);
}
$lit_manifest = ['divi_version' => wp_get_theme('Divi')->get('Version'), 'posts' => []];
foreach ($lit_ids as $postId) {
$post = get_post($postId);
$terms = $lit_terms($postId);
$role = $post->post_type === 'et_template' && get_post_meta($postId, '_et_default', true) === '1'
? 'tb-template--default'
: $lit_role($post, $terms);
$record = [
'post' => [
'ID' => $post->ID,
'post_type' => $post->post_type,
'post_status' => $post->post_status,
'post_title' => $post->post_title,
'post_name' => $post->post_name,
'post_author' => (int) $post->post_author,
'post_parent' => $post->post_parent,
'menu_order' => $post->menu_order,
'post_excerpt' => $post->post_excerpt,
'post_date_gmt' => $post->post_date_gmt,
'post_modified_gmt' => $post->post_modified_gmt,
'content_file' => $post->post_content === '' ? null : $role . '.html',
'content_sha256' => hash('sha256', $post->post_content),
],
'meta' => (object) $lit_meta($postId),
'terms' => (object) $terms,
];
$lit_write($lit_dir . '/' . $role . '.json', $lit_json($record));
if ($post->post_content !== '') {
$lit_write($lit_dir . '/' . $role . '.html', $post->post_content);
}
$lit_manifest['posts'][$role] = $postId;
}
$lit_options = [];
$lit_option_rows = $wpdb->get_results(
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'et\\_tb\\_%' ORDER BY option_name"
);
foreach ($lit_option_rows as $row) {
$lit_options[$row->option_name] = $row->option_value;
}
$lit_write($lit_dir . '/theme-builder.json', $lit_json([
'theme_builder_post' => $lit_tb_id ? [
'ID' => $lit_tb_id,
'post_status' => get_post_status($lit_tb_id),
'post_title' => get_the_title($lit_tb_id),
'meta' => (object) $lit_meta($lit_tb_id),
] : null,
'template_ids_in_order' => $lit_tb_templates,
'draft_theme_builder_posts' => array_map('intval', $wpdb->get_col(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'et_theme_builder' AND post_status = 'auto-draft'"
)),
'options_et_tb' => (object) $lit_options,
'marked_as_unused' => array_map('intval', $wpdb->get_col(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_et_theme_builder_marked_as_unused'"
)),
]));
$lit_write($lit_dir . '/manifest.json', $lit_json($lit_manifest));
WP_CLI::success(sprintf('%d posts exported to %s', count($lit_manifest['posts']), $lit_dir));