feat(frontend): replace article system with Zibll 9.0

This commit is contained in:
2026-08-18 16:06:47 +08:00
parent 2881c69e9c
commit 0f2ac6c34a
69 changed files with 2603 additions and 4163 deletions
+31 -7
View File
@@ -8,16 +8,40 @@ $env:ZIBLL_THEME_PATH='C:\Users\Xing\Desktop\zibll'
docker compose -f tests/visual/docker-compose.zibll.yml up -d
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp core install --url=https://localhost:9443 --title='Zibll Reference' --admin_user=visual_admin --admin_password='AKSRT-Visual-2026!' --admin_email=visual@example.test --skip-email
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp theme activate zibll
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp eval-file /visual-fixtures/article-fixtures.php create
~~~
Create or clean AKSRT fixtures inside its WordPress container:
Run the same theme-neutral article generator inside the AKSRT WordPress
container. Mount `tests/visual/` read-only at `/visual-fixtures` in that stack:
~~~powershell
wp eval-file wp-content/themes/AKSRT-theme/tools/wp-cli-visual-fixtures.php create
wp eval-file wp-content/themes/AKSRT-theme/tools/wp-cli-visual-fixtures.php cleanup
wp eval-file /visual-fixtures/article-fixtures.php create
wp eval-file /visual-fixtures/article-fixtures.php cleanup
~~~
Use tests/fixtures/widget-golden-v1.json for the 222 component captures and
tests/fixtures/page-visual-matrix-v1.json for page-level captures. Remove
the reference stack with docker compose down -v only after fixture cleanup
and screenshot verification.
The shared generator creates the same authors, category, tag, dates, images,
featured/no-image/multi-image/first-image/external-cover posts, slide and video
metadata, pay/VIP states, sticky state, and interaction counters in both sites.
Fixture-only Zibll and AKSRT metadata are written together so the rendered
business state is identical without adding compatibility code to either theme.
Use `tests/visual/article-visual-matrix-v1.json` for the article-only captures.
Every reference/candidate pair must record the screenshot, DOM, computed styles,
bounding boxes, console messages, and failed network requests. The matrix pins
1440, 1024, 768, 767, 641, 640, and 390 widths across light/dark,
sidebar/no-sidebar, list/card modes, media states, and all pagination states.
The acceptance gate is at most 1 CSS px geometry delta and 1.5% pixel
difference, with no visible mismatch, console error, unexpected failed request,
or critical/serious accessibility issue.
Clean the reference fixture before removing its volumes:
~~~powershell
docker compose -f tests/visual/docker-compose.zibll.yml run --rm wp eval-file /visual-fixtures/article-fixtures.php cleanup
docker compose -f tests/visual/docker-compose.zibll.yml down -v
~~~
The older `tools/wp-cli-visual-fixtures.php`, widget golden file, and broad page
matrix remain dedicated to non-article component regression tests and are not
the truth source for this replacement.
+294
View File
@@ -0,0 +1,294 @@
<?php
/**
* Theme-neutral article fixtures shared by the Zibll and AKSRT visual sites.
*
* Usage: wp eval-file /visual-fixtures/article-fixtures.php create|cleanup
*/
defined('ABSPATH') || exit;
if (!defined('WP_CLI') || !WP_CLI) {
return;
}
$action = sanitize_key((string) ($args[0] ?? ''));
$manifest_key = 'zibll_article_visual_fixture_v1';
$prefix = 'zibll_article_v1_';
if (!in_array($action, ['create', 'cleanup'], true)) {
WP_CLI::error('Expected create or cleanup.');
}
$cleanup = static function (array $manifest) use ($manifest_key): void {
require_once ABSPATH . 'wp-admin/includes/user.php';
foreach (array_reverse((array) ($manifest['posts'] ?? [])) as $post_id) {
unstick_post((int) $post_id);
wp_delete_post((int) $post_id, true);
}
foreach (array_reverse((array) ($manifest['attachments'] ?? [])) as $attachment_id) {
wp_delete_attachment((int) $attachment_id, true);
}
$upload = wp_upload_dir();
$upload_root = wp_normalize_path((string) ($upload['basedir'] ?? ''));
foreach (array_reverse((array) ($manifest['files'] ?? [])) as $file) {
$file = wp_normalize_path((string) $file);
if ($upload_root !== '' && str_starts_with($file, trailingslashit($upload_root)) && is_file($file)) {
unlink($file);
}
}
foreach (array_reverse((array) ($manifest['terms'] ?? [])) as $term) {
wp_delete_term((int) $term['id'], (string) $term['taxonomy']);
}
foreach (array_reverse((array) ($manifest['users'] ?? [])) as $user_id) {
wp_delete_user((int) $user_id);
}
foreach ((array) ($manifest['options'] ?? []) as $name => $state) {
if (!empty($state['exists'])) {
update_option((string) $name, $state['value'], false);
} else {
delete_option((string) $name);
}
}
delete_option($manifest_key);
};
$existing = get_option($manifest_key, []);
if ($action === 'cleanup') {
if (!is_array($existing) || !$existing) {
WP_CLI::success('No article visual fixtures found.');
return;
}
$cleanup($existing);
WP_CLI::success('Article visual fixtures removed.');
return;
}
if (is_array($existing) && $existing) {
WP_CLI::error('Fixtures already exist. Run cleanup first.');
}
$manifest = [
'created_at' => gmdate('c'),
'users' => [],
'terms' => [],
'attachments' => [],
'posts' => [],
'files' => [],
'options' => [],
];
foreach (['aksrt_settings', 'zibll_options'] as $option_name) {
$manifest['options'][$option_name] = [
'exists' => get_option($option_name, null) !== null,
'value' => get_option($option_name),
];
}
update_option($manifest_key, $manifest, false);
try {
$visual_options = [
'list_type' => 'thumb',
'list_show_type' => 'separate',
'list_list_option' => ['style' => 'null', 'img_position' => 'left', 'scale' => 70],
'list_card_option' => ['style' => 'null', 'scale' => 70],
'post_list_author' => true,
'post_list_time' => true,
'post_time_source' => 'modified',
'list_meta_show' => 'like',
'list_badge_show' => ['pay', 'cat', 'tag'],
'list_thumb_slides_s' => true,
'list_thumb_video_s' => true,
'list_thumb_video_mute_s' => true,
'thumb_postfirstimg_s' => true,
'thumb_catimg_s' => true,
'list_thumb_fit' => '',
'paging_ajax_s' => '1',
'paging_ajax_ias_s' => true,
'ias_max' => 3,
'post_related_s' => true,
'post_related_type' => 'img',
'post_related_n' => 6,
'post_related_orderby' => 'views',
'related_title' => '相关推荐',
];
foreach (['aksrt_settings', 'zibll_options'] as $option_name) {
$current = get_option($option_name, []);
update_option($option_name, array_merge(is_array($current) ? $current : [], $visual_options), false);
}
$authors = [
['login' => $prefix . 'author_a', 'name' => '视觉作者甲'],
['login' => $prefix . 'author_b', 'name' => '视觉作者乙'],
];
$author_ids = [];
foreach ($authors as $author) {
$user_id = wp_insert_user([
'user_login' => $author['login'],
'user_pass' => 'AKSRT-Visual-2026!',
'user_email' => $author['login'] . '@example.test',
'display_name' => $author['name'],
'description' => '固定作者简介,用于文章卡片视觉对比。',
'role' => 'author',
]);
if (is_wp_error($user_id)) {
throw new RuntimeException($user_id->get_error_message());
}
$author_ids[] = (int) $user_id;
$manifest['users'][] = (int) $user_id;
}
$category = wp_insert_term('视觉文章', 'category', ['slug' => 'zibll-article-fixture']);
$tag = wp_insert_term('视觉标签', 'post_tag', ['slug' => 'zibll-visual-tag']);
if (is_wp_error($category) || is_wp_error($tag)) {
throw new RuntimeException('Unable to create article fixture terms.');
}
$category_id = (int) $category['term_id'];
$tag_id = (int) $tag['term_id'];
$manifest['terms'][] = ['id' => $category_id, 'taxonomy' => 'category'];
$manifest['terms'][] = ['id' => $tag_id, 'taxonomy' => 'post_tag'];
require_once ABSPATH . 'wp-admin/includes/image.php';
$upload = wp_upload_dir();
if (!empty($upload['error'])) {
throw new RuntimeException((string) $upload['error']);
}
$attachment_ids = [];
$colors = [
[240, 68, 148],
[58, 134, 255],
[92, 191, 147],
[255, 175, 56],
[126, 87, 194],
];
foreach ($colors as $index => $rgb) {
$filename = $prefix . 'image-' . ($index + 1) . '.jpg';
$filepath = trailingslashit($upload['path']) . $filename;
if (function_exists('imagecreatetruecolor')) {
$image = imagecreatetruecolor(1200, 800);
$background = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
$foreground = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $background);
imagestring($image, 5, 48, 48, 'Zibll 9.0 Article Fixture ' . ($index + 1), $foreground);
imagejpeg($image, $filepath, 92);
imagedestroy($image);
} else {
$pixel = base64_decode('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAX/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAEf/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k=', true);
file_put_contents($filepath, $pixel);
}
$attachment_id = wp_insert_attachment([
'post_mime_type' => 'image/jpeg',
'post_title' => '视觉图片 ' . ($index + 1),
'post_status' => 'inherit',
], $filepath);
if (is_wp_error($attachment_id)) {
throw new RuntimeException($attachment_id->get_error_message());
}
$attachment_id = (int) $attachment_id;
wp_update_attachment_metadata($attachment_id, wp_generate_attachment_metadata($attachment_id, $filepath));
$attachment_ids[] = $attachment_id;
$manifest['attachments'][] = $attachment_id;
}
$image_urls = array_map(static fn(int $id): string => (string) wp_get_attachment_image_url($id, 'full'), $attachment_ids);
$video_path = trailingslashit($upload['path']) . $prefix . 'fixture-video.mp4';
file_put_contents($video_path, '');
$manifest['files'][] = $video_path;
$video_url = trailingslashit($upload['url']) . basename($video_path);
$term_other = (array) get_term_meta($category_id, 'zib_other_data', true);
$term_other['cover_image'] = $image_urls[4];
update_term_meta($category_id, 'zib_other_data', $term_other);
update_term_meta($category_id, 'cover_image', $image_urls[4]);
update_term_meta($category_id, 'aksrt_category_cover_id', $attachment_ids[4]);
$scenarios = [
['slug' => 'featured', 'title' => '特色图文章:标准列表与卡片基线', 'featured' => 0],
['slug' => 'no-image', 'title' => '无图长标题文章用于检查纯文字模式与两行截断', 'no_image' => true],
['slug' => 'multi', 'title' => '多图文章:四张真实内容图片', 'featured' => 1, 'gallery' => true],
['slug' => 'external', 'title' => '自定义外链封面文章', 'external' => 2],
['slug' => 'slides', 'title' => '幻灯片封面文章', 'featured' => 3, 'slides' => true],
['slug' => 'video', 'title' => '视频悬停封面文章', 'featured' => 4, 'video' => true],
['slug' => 'paid', 'title' => '付费阅读文章价格徽章', 'featured' => 0, 'pay' => 'reading'],
['slug' => 'vip', 'title' => '会员专属文章 VIP 徽章', 'featured' => 1, 'pay' => 'vip'],
['slug' => 'first-image', 'title' => '文章首图作为缩略图', 'first_image' => 3],
];
foreach ($scenarios as $index => $scenario) {
$content = '<p>固定摘要正文,用于 Zibll 与 AKSRT 的文章列表逐像素比较。</p>';
if (!empty($scenario['gallery'])) {
$content .= implode('', array_map(static fn(string $url): string => '<img src="' . esc_url($url) . '" alt="">', array_slice($image_urls, 0, 4)));
} elseif (isset($scenario['first_image'])) {
$content .= '<img src="' . esc_url($image_urls[(int) $scenario['first_image']]) . '" alt="">';
}
$post_id = wp_insert_post([
'post_type' => 'post',
'post_status' => 'publish',
'post_author' => $author_ids[$index % 2],
'post_title' => $scenario['title'],
'post_name' => $prefix . $scenario['slug'],
'post_excerpt' => '固定文章摘要用于检查字号、行高、留白和截断。',
'post_content' => $content,
'post_date' => gmdate('Y-m-d H:i:s', strtotime('2026-07-01 08:00:00') + ($index * DAY_IN_SECONDS)),
'post_date_gmt' => gmdate('Y-m-d H:i:s', strtotime('2026-07-01 00:00:00') + ($index * DAY_IN_SECONDS)),
], true);
if (is_wp_error($post_id)) {
throw new RuntimeException($post_id->get_error_message());
}
$post_id = (int) $post_id;
$manifest['posts'][] = $post_id;
wp_set_post_terms($post_id, [$category_id], 'category');
wp_set_post_terms($post_id, [$tag_id], 'post_tag');
if (isset($scenario['featured'])) {
set_post_thumbnail($post_id, $attachment_ids[(int) $scenario['featured']]);
}
if (!empty($scenario['gallery'])) {
set_post_format($post_id, 'gallery');
}
$post_meta = [
'subtitle' => ' · 固定副标题',
];
$zib_meta = (array) get_post_meta($post_id, 'zib_other_data', true);
$zib_meta['subtitle'] = $post_meta['subtitle'];
if (isset($scenario['external'])) {
$post_meta['thumbnail_url'] = $image_urls[(int) $scenario['external']];
$zib_meta['thumbnail_url'] = $post_meta['thumbnail_url'];
}
if (!empty($scenario['slides'])) {
$post_meta['featured_slide'] = implode(',', array_slice($attachment_ids, 0, 4));
$zib_meta['featured_slide'] = $post_meta['featured_slide'];
}
if (!empty($scenario['video'])) {
$post_meta['featured_video'] = $video_url;
$zib_meta['featured_video'] = $video_url;
}
$zib_meta['views'] = 1200 + ($index * 137);
$zib_meta['like'] = 30 + ($index * 7);
update_post_meta($post_id, 'zib_other_data', $zib_meta);
update_post_meta($post_id, 'aksrt_post_meta', $post_meta);
update_post_meta($post_id, 'aksrt_post_views', $zib_meta['views']);
update_post_meta($post_id, '_aksrt_like_count', $zib_meta['like']);
update_post_meta($post_id, '_aksrt_favorite_count', 10 + $index);
$pay_type = (string) ($scenario['pay'] ?? 'no');
$pay_limit = $pay_type === 'vip' ? 2 : 0;
$pay_price = $pay_type === 'reading' ? 19.9 : 0;
update_post_meta($post_id, '_aksrt_pay', [
'pay_type' => $pay_type,
'pay_modo' => '0',
'price' => $pay_price,
'pay_limit' => $pay_limit,
]);
update_post_meta($post_id, 'posts_zibpay', [
'pay_type' => $pay_type,
'pay_modo' => '0',
'pay_price' => $pay_price,
'pay_limit' => $pay_limit,
]);
}
if ($manifest['posts']) {
stick_post((int) $manifest['posts'][0]);
}
update_option($manifest_key, $manifest, false);
WP_CLI::success('Created identical Zibll/AKSRT article fixtures.');
} catch (Throwable $error) {
$cleanup($manifest);
WP_CLI::error('Fixture creation rolled back: ' . $error->getMessage());
}
@@ -0,0 +1,45 @@
{
"schema": "aksrt.zibll-article-visual-matrix.v1",
"reference": "https://localhost:9443",
"candidate": "https://localhost",
"fixture": "/visual-fixtures/article-fixtures.php",
"viewports": [
{ "id": "desktop-wide", "width": 1440, "height": 1100 },
{ "id": "desktop", "width": 1024, "height": 1000 },
{ "id": "tablet", "width": 768, "height": 1000 },
{ "id": "breakpoint-767", "width": 767, "height": 1000 },
{ "id": "breakpoint-641", "width": 641, "height": 960 },
{ "id": "breakpoint-640", "width": 640, "height": 960 },
{ "id": "mobile", "width": 390, "height": 844 }
],
"colorSchemes": ["light", "dark"],
"layouts": ["sidebar", "no-sidebar"],
"listModes": ["thumb", "text", "thumb_if_has", "card"],
"styles": ["default", "style2", "style3"],
"routes": [
{ "id": "home", "path": "/", "states": ["default", "hover", "loading", "error"] },
{ "id": "archive", "path": "/?post_type=post", "states": ["default", "numbered", "load-more", "infinite", "no-more"] },
{ "id": "category", "path": "/category/zibll-article-fixture/", "states": ["default", "empty"] },
{ "id": "tag", "path": "/tag/zibll-visual-tag/", "states": ["default", "empty"] },
{ "id": "search", "path": "/?s=视觉", "states": ["default", "empty"] },
{ "id": "author-a", "path": "/author/zibll_article_v1_author_a/", "states": ["default", "numbered", "load-more"] },
{ "id": "single-related", "path": "/zibll_article_v1_featured/", "states": ["img", "list", "text"] }
],
"postScenarios": ["featured", "no-image", "multi", "external", "slides", "video", "paid", "vip", "first-image"],
"capture": {
"selector": ".posts-row, .profile-list, .relates",
"artifacts": ["screenshot", "dom", "computed-style", "bounding-box", "console", "network"],
"computedProperties": [
"display", "position", "width", "height", "margin", "padding", "gap", "font-size", "line-height",
"border-radius", "box-shadow", "background-color", "color", "object-fit"
]
},
"acceptance": {
"dimensionDeltaPxMax": 1,
"pixelDifferencePercentMax": 1.5,
"consoleErrorsMax": 0,
"unexpectedNetworkFailuresMax": 0,
"a11yCriticalMax": 0,
"a11ySeriousMax": 0
}
}
+8
View File
@@ -32,6 +32,10 @@ services:
source: ${ZIBLL_THEME_PATH}
target: /var/www/html/wp-content/themes/zibll
read_only: true
- type: bind
source: .
target: /visual-fixtures
read_only: true
caddy:
image: caddy:2.8-alpine
@@ -62,6 +66,10 @@ services:
source: ${ZIBLL_THEME_PATH}
target: /var/www/html/wp-content/themes/zibll
read_only: true
- type: bind
source: .
target: /visual-fixtures
read_only: true
entrypoint: ["wp"]
volumes:
@@ -0,0 +1,39 @@
{
"schema": "aksrt.zibll-article-baseline.v1",
"referencePath": "C:\\Users\\Xing\\Desktop\\zibll",
"theme": "Zibll",
"version": "9.0",
"capturedAt": "2026-08-18",
"files": {
"style.css": {
"bytes": 927,
"sha256": "4e9e319d92cea3862aefd520d5519b81ea22f9147b203b77eafedd0447148aea"
},
"css/main.css": {
"bytes": 292331,
"sha256": "87ff1aeaf78369934c875422ab3ebc13ad65f30de78f6caabf091c780a27a158"
},
"inc/functions/zib-posts-list.php": {
"bytes": 34255,
"sha256": "ced813182c0b0fc5847ca5c04a57fa4379581ace13a6d40dfb7dfabb55e13ae4"
},
"inc/options/admin-options.php": {
"bytes": 700302,
"sha256": "3d2cf89bc01a1ec98297d16c14023a820170568e716daedf421fd0386b38486b"
},
"js/main.min.js": {
"bytes": 74446,
"sha256": "0fdd02246a5b68480d11ced1dfd2bfa19788fef16ba069eb021fcfb5cfaf60f9"
},
"js/libs/grade.min.js": {
"bytes": 4833,
"sha256": "8d74bf6ab34f4c44203766cc639818c4daf0a7c1978ddefb4ebfde9daaa16040"
}
},
"excluded": [
"topics taxonomy",
"single article body",
"comments",
"header and footer"
]
}