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"
]
}
+241 -62
View File
@@ -7,79 +7,258 @@ const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const join = (files) => files.map(read).join('\n');
test('post list DOM follows the Zibll flex and multi-image contract', () => {
const articleFiles = [
'src/Settings/PostDisplay.php',
'src/Admin/Schema/PostList/SchemaPostListList.php',
'src/Admin/Schema/SchemaPostList.php',
'assets/admin/src/schema/sections/post-list/list.js',
'assets/admin/src/schema/sections/post-list/thumb.js',
'assets/admin/src/schema/sections/post-list/page.js',
'inc/helpers/posts-renderer/config.php',
'inc/helpers/posts-renderer/post-data.php',
'inc/helpers/posts-renderer/markup.php',
'inc/helpers/aksrt-archive.php',
'inc/helpers/aksrt-single-render.php',
];
test('the article stream exposes only the new typed renderer contract', () => {
const markup = read('inc/helpers/posts-renderer/markup.php');
const consumers = join([
'index.php',
'category.php',
'tag.php',
'search.php',
'archive-resources.php',
'src/Admin/Admin.php',
'src/Admin/GlobalController.php',
'src/Core/Shortcodes.php',
'src/WidgetSystem/PostRenderer.php',
'inc/helpers/homepage/content.php',
'templates/parts/user-home/panels-core.php',
]);
assert.match(markup, /function aksrt_render_post_list\(array \$args = \[\], \?WP_Query \$query = null, bool \$echo = true\): string/);
assert.doesNotMatch(consumers, /function_exists\(['"]aksrt_render_post_list/);
assert.doesNotMatch(consumers, /aksrt_render_post_list\(\s*\$query\s*,/);
assert.doesNotMatch(consumers, /content-post-card|aksrt_homepage_render_post_card/);
});
test('settings use Zibll list modes, grouped options, and destructive related keys', () => {
const source = join(articleFiles);
const listSchema = read('assets/admin/src/schema/sections/post-list/list.js');
const pageSchema = read('assets/admin/src/schema/sections/post-list/page.js');
const legacyKeys = [
'post_card_style',
'list_list_style',
'list_card_style',
'grid-cms',
'large_thumb',
'multi_thumb',
'layout_override',
'homepage_card_style',
'homepage_multi_thumb',
'homepage_default_view',
'post_related_enabled',
'post_related_count',
'post_related_style',
'post_related_source',
'post_related_title',
];
assert.match(listSchema, /list_type: 'thumb'/);
for (const value of ['text', 'thumb', 'thumb_if_has', 'card']) {
assert.match(listSchema, new RegExp(`value: '${value}'`));
}
assert.match(listSchema, /list_list_option: \{ style: 'null', img_position: 'left', scale: 70 \}/);
assert.match(listSchema, /list_card_option: \{ style: 'null', scale: 70 \}/);
assert.match(pageSchema, /post_related_s: true/);
assert.match(pageSchema, /post_related_type: 'img'/);
assert.match(pageSchema, /related_title: '相关推荐'/);
assert.match(pageSchema, /post_related_n: 6/);
assert.match(pageSchema, /post_related_orderby: 'views'/);
for (const key of legacyKeys) {
assert.equal(source.includes(key), false, `legacy article key remains: ${key}`);
}
assert.doesNotMatch(source, /list_card_topics|is_topics|\btopics\b/);
});
test('forced mode wins and thumb_if_has ignores spare and lazy placeholders', () => {
const config = read('inc/helpers/posts-renderer/config.php');
const data = read('inc/helpers/posts-renderer/post-data.php');
const forced = config.indexOf("$is_forced = in_array($forced_type, ['list', 'card'], true)");
const globalCard = config.indexOf("$list_type === 'card'");
const contextCard = config.indexOf("$list['list_card_tag']");
const categoryCard = config.indexOf("$list['list_card_cat']");
assert.ok(forced >= 0 && forced < globalCard && globalCard < contextCard && contextCard < categoryCard);
assert.match(config, /\$is_card = \$forced_type === 'card' \|\| \(!\$is_forced/);
const hasReal = config.indexOf("$has_real = $primary !== '';");
const spareCall = config.lastIndexOf('aksrt_renderer_spare_image($thumb)');
const lazy = config.lastIndexOf('aksrt_get_lazy_thumb');
assert.ok(hasReal >= 0 && hasReal < spareCall && spareCall < lazy);
assert.match(data, /'thumb_if_has' && empty\(\$images\['has_real'\]\)/);
assert.match(data, /count\(\$gallery\) > 2/);
assert.match(data, /has_post_format\(\['image', 'gallery'\]/);
});
test('image resolution counts only content images and applies the configured WordPress size', () => {
const config = read('inc/helpers/posts-renderer/config.php');
const data = read('inc/helpers/posts-renderer/post-data.php');
const thumbSchema = read('assets/admin/src/schema/sections/post-list/thumb.js');
const phpSchema = read('src/Admin/Schema/SchemaPostList.php');
assert.match(config, /function aksrt_renderer_content_images\(int \$post_id, string \$size\)/);
assert.match(config, /strip_shortcodes\(\(string\) get_post_field\('post_content'/);
assert.match(config, /\$slides = aksrt_renderer_curated_images\(\$post_id, \$size\);\s*\$gallery = aksrt_renderer_content_images\(\$post_id, \$size\);/);
assert.match(config, /get_post_thumbnail_id\(\$post_id\).*aksrt_renderer_sized_image\(\(string\) \$thumbnail_id, \$size\)/s);
assert.match(config, /aksrt_renderer_category_image\(\$post_id, \$size\)/);
assert.doesNotMatch(config, /aksrt_get_post_gallery_images|cover_images|featured_video_episode|_aksrt_featured_video|_video_url|_aksrt_thumbnail_url|_thumbnail_url/);
assert.doesNotMatch(config, /\['aksrt_category_cover_id',/);
assert.match(data, /'gallery_count'\s*=>\s*count\(\$content_images\)/);
assert.match(data, /\(int\) \$data\['gallery_count'\] - 4/);
assert.match(thumbSchema, /spareThumbnailDefault = \[\{ img: `\$\{themeRoot\}\/assets\/img\/thumbnail\.svg` \}\]/);
assert.match(phpSchema, /'spare_thumbnail'.*'default' => \[\['img' => get_template_directory_uri\(\) \. '\/assets\/img\/thumbnail\.svg'\]\]/);
});
test('list, card, media, badge and Mini DOM follow dedicated Zibll structures', () => {
const markup = read('inc/helpers/posts-renderer/markup.php');
const data = read('inc/helpers/posts-renderer/post-data.php');
const cardStart = markup.indexOf('function aksrt_renderer_card');
const miniStart = markup.indexOf('function aksrt_renderer_mini');
const card = markup.slice(cardStart, miniStart);
const card = markup.slice(markup.indexOf('function aksrt_renderer_card'), markup.indexOf('function aksrt_renderer_mini'));
const mini = markup.slice(markup.indexOf('function aksrt_renderer_mini'), markup.indexOf('function aksrt_render_post'));
assert.match(markup, /\['posts-item', 'list', 'ajax-item', 'flex'/);
assert.match(markup, /if \(!empty\(\$data\['is_mult_thumb'\]\)\)[\s\S]*?\$thumb = '';/);
assert.doesNotMatch(card, /item-excerpt|renderer_excerpt_markup/);
assert.match(data, /&& \$cfg\['mode'\] === 'list'/);
assert.match(data, /<a class=\"thumb-items contain\"/);
assert.match(markup, /\['posts-item', 'list', 'ajax-item'\]/);
assert.match(markup, /\$classes\[\] = .*'flex xx'.*'flex'/s);
assert.match(markup, /<posts class=/);
assert.match(markup, /<div class="post-graphic/);
assert.match(markup, /<div class="item-body flex xx flex1 jsb/);
assert.doesNotMatch(card, /item-excerpt|renderer_excerpt_markup|thumb-items/);
assert.match(data, /<div class="item-thumbnail/);
assert.match(data, /relative zib-slider.*new-swiper item-thumbnail/s);
assert.match(data, /swiper-slide.*gradient-bg.*data-opacity="0\.2"/s);
assert.match(data, /\$fit = \$is_contain && !\$has_slides \? ' contain gradient-bg' : ''/);
assert.match(data, /video-thumb-box.*data-post-thumb-video/s);
assert.match(data, /class="thumb-items/);
assert.match(data, /empty\(\$cfg\['is_category'\]\).*in_array\('cat'/s);
assert.match(data, /empty\(\$cfg\['is_tag'\]\).*in_array\('tag'/s);
assert.doesNotMatch(data, /meta-mobile-show/);
assert.match(mini, /<div class="mr10"><div class="item-thumbnail"><a/);
assert.match(mini, /posts-mini-con flex xx flex1 jsb/);
assert.match(mini, /\['c-red', 'c-yellow', 'c-purple', 'c-blue', 'c-green'\]/);
assert.match(mini, /aksrt_renderer_pay_badge\(\$data, true\)/);
assert.match(data, /meta-pay badg badg-sm mr6/);
assert.doesNotMatch(mini, /posts-mini-row/);
assert.doesNotMatch(markup, /--posts-(?:list|card)-scale|<posts[^>]+style=/);
assert.match(data, /\['c-blue', 'c-yellow', 'c-green', 'c-purple', 'c-red'\]/);
assert.match(data, /查看更多分类文章/);
assert.match(data, /usort\(\$tags,.*->count.*->count/s);
assert.match(data, /查看此标签更多文章/);
const commentMeta = data.indexOf("aksrt_renderer_svg_icon('comment')");
const viewMeta = data.indexOf("aksrt_renderer_svg_icon('view')", commentMeta);
const likeMeta = data.indexOf("aksrt_renderer_svg_icon('like')", viewMeta);
assert.ok(commentMeta >= 0 && commentMeta < viewMeta && viewMeta < likeMeta);
assert.match(data, /comments_open.*close_comments/s);
assert.match(data, /show_like.*post_type.*=== 'post'/s);
assert.match(data, /<use xlink:href="#icon-/);
});
test('homepage card mode keeps the Zibll inline-block three-column flow', () => {
const css = read('assets/css/frontend/homepage-fixed/base-2.css');
const responsive = read('assets/css/frontend/homepage-fixed/ajax-tab-error-state.css');
const homepageScript = read('assets/js/homepage-fixed.js');
const scripts = read('src/Core/ScriptsPart1b.php');
const cardModeStart = css.indexOf('.aksrt-homepage__posts[data-view="card"] {');
const cardItemStart = css.indexOf('.aksrt-homepage__posts[data-view="card"] > .posts-item {');
const cardMode = css.slice(cardModeStart, cardItemStart);
const cardItem = css.slice(cardItemStart, css.indexOf('\n}', cardItemStart) + 2);
const visibilityStart = css.indexOf('VISIBILITY LOCK');
const visibilityEnd = css.indexOf('/* Minimal flex utilities', visibilityStart);
const visibility = css.slice(visibilityStart, visibilityEnd);
test('post-card CSS owns the exact desktop and 767/640 responsive matrix', () => {
const css = read('assets/css/frontend/post-card.css');
const mobile767 = css.slice(css.indexOf('@media (max-width: 767px)'), css.indexOf('@media (max-width: 640px)'));
const mobile640 = css.slice(css.indexOf('@media (max-width: 640px)'));
assert.match(cardMode, /display:\s*block/);
assert.match(cardMode, /font-size:\s*0/);
assert.match(cardItem, /width:\s*calc\(33\.333% - 16px\)/);
assert.match(cardItem, /display:\s*inline-block/);
assert.doesNotMatch(visibility, /\.posts-item[\s\S]*?display:\s*block\s*!important/);
assert.match(scripts, /\$homepage_style_parts\s*=\s*\[/);
assert.match(scripts, /filemtime\(\$part_path\)/);
assert.doesNotMatch(scripts, /wp_enqueue_style\(\s*'aksrt-homepage-fixed',\s*\$base_uri\s*\.\s*'\/assets\/css\/frontend\/homepage-fixed\.css'/);
const tabletStart = responsive.indexOf('@media (max-width: 991.98px)');
const mobileStart = responsive.indexOf('@media (max-width: 767px)');
const smallMobileStart = responsive.indexOf('@media (max-width: 640px)');
const tablet = responsive.slice(tabletStart, mobileStart);
const mobile = responsive.slice(mobileStart, smallMobileStart);
const smallMobile = responsive.slice(smallMobileStart);
assert.doesNotMatch(tablet, /data-view="card"/);
assert.match(mobile, /data-view="card"[\s\S]*?width:\s*calc\(50% - 10px\)/);
assert.match(mobile, /data-view="card"[\s\S]*?margin:\s*-5px/);
assert.doesNotMatch(smallMobile, /data-view="card"[\s\S]*?width:\s*100%/);
assert.match(homepageScript, /var initialMode = getViewMode\(\)/);
assert.match(homepageScript, /var renderedMode = activePanel \? activePanel\.getAttribute\('data-view'\)/);
assert.match(homepageScript, /renderedMode !== initialMode[\s\S]*?requestHomepagePanel\(activePanel/);
assert.match(css, /\.posts-row\s*\{[^}]*margin-right:\s*-8px;[^}]*margin-left:\s*-8px;/s);
assert.match(css, /\.posts-item\.card\s*\{[^}]*width:\s*calc\(33\.333% - 16px\)/s);
assert.match(css, /\.fluid-widget-wrap \.posts-item\.card,[\s\S]*?\.site-layout-1 \.posts-item\.card\s*\{[^}]*width:\s*calc\(25% - 16px\)/);
assert.match(css, /\.site-layout-1 \.posts-item:not\(\.card\)\s*\{[^}]*width:\s*calc\(50% - 16px\)/s);
assert.match(mobile767, /\.posts-item\.card\s*\{[^}]*width:\s*calc\(50% - 10px\)/s);
assert.doesNotMatch(mobile767, /\.posts-mini\s*\{[^}]*width:\s*100%/s);
assert.match(mobile640, /\.posts-mini\s*\{[^}]*width:\s*100%/s);
assert.match(mobile640, /\.posts-item:not\(\.no-thumb\) \.meta-right item\s*\{[^}]*display:\s*none/s);
assert.doesNotMatch(css, /\.aksrt-|post-card-cms|grid-cms/);
});
test('site shell has one float owner and fluid zones sit outside main', () => {
const layout = read('assets/css/frontend/layout.css');
const archive = read('inc/helpers/aksrt-archive.php');
const shell = layout.slice(layout.indexOf('Site shell'), layout.indexOf('页面标题区'));
test('related posts, author page and widgets use their dedicated renderers', () => {
const related = read('inc/helpers/aksrt-single-render.php');
const relatedCard = read('inc/helpers/aksrt-single-components.php');
const resourceRelated = read('templates/parts/resource-related.php');
const author = read('templates/parts/user-home/panels-core.php');
const authorPosts = author.slice(author.indexOf("in_array('posts'"), author.indexOf("in_array('comments'"));
const widgets = read('src/WidgetSystem/PostRenderer.php');
assert.doesNotMatch(shell, /display:\s*grid/);
assert.match(shell, /width:\s*var\(--sidebar-width, 311px\)/);
assert.match(shell, /calc\(var\(--sidebar-width, 311px\) \+ 15px\)/);
assert.ok(archive.indexOf("$args['top_fluid']") < archive.indexOf('<main id="main-content"'));
assert.ok(archive.lastIndexOf("$args['bottom_fluid']") > archive.indexOf('</main>'));
assert.match(related, /\['img', 'list', 'text'\]/);
assert.match(related, /aksrt_renderer_mini\(\$data, \$mini_cfg\)/);
assert.match(related, /aksrt_render_related_graphic_card\(\$post\)/);
assert.match(related, /<li><a class="icon-circle"/);
assert.doesNotMatch(related, /topics|post_related_source/);
assert.match(relatedCard, /aksrt_renderer_resolve_images/);
assert.match(resourceRelated, /swiper-container swiper-scroll/);
assert.match(resourceRelated, /aksrt_render_related_graphic_card/);
assert.doesNotMatch(resourceRelated, /aksrt_render_post_list/);
assert.match(authorPosts, /aksrt_render_post_list/);
assert.match(authorPosts, /aksrt_render_pagination/);
assert.match(authorPosts, /'page_var'\s*=>\s*'posts_page'/);
assert.doesNotMatch(authorPosts, /aksrt_render_profile_pagination/);
assert.match(widgets, /aksrt_render_mini_list/);
assert.match(widgets, /swiper-container swiper-scroll/);
});
test('widget shell uses Zibll wrappers without overriding post link colors', () => {
const renderer = read('src/WidgetSystem/FrontendRenderer.php');
const css = read('assets/widgets/frontend.css');
test('AJAX pagination can recover, caps auto loading and rehydrates appended media', () => {
const php = read('inc/helpers/template-tags/pagination-permissions.php');
const admin = read('src/Admin/Admin.php');
const js = read('assets/js/core/pagination.js');
const media = read('assets/js/core/misc.js');
assert.match(renderer, /'zib-widget-wrap'/);
assert.match(renderer, /\$classes\[\] = 'zib-widget'/);
assert.match(renderer, /aksrt-widget__inner container/);
assert.doesNotMatch(css, /\.aksrt-widget a\s*\{[^}]*color:\s*inherit/);
assert.match(css, /\.aksrt-widget\.zib-widget\s*\{/);
assert.match(php, /class="text-center theme-pagination ajax-pag"/);
assert.match(php, /class="next-page ajax-next/);
assert.match(php, /lazyload-action="ias"/);
assert.match(php, /class="post_ajax_loader"/);
assert.match(php, /data-page-var/);
assert.match(admin, /'author', 'author_name'/);
assert.match(admin, /'category__in'.*'tag__in'/s);
assert.match(admin, /\$query_vars\['paged'\] = \$page/);
assert.match(js, /failed = false;\s*loadNextPage\(fromAuto\)/);
assert.match(js, /successfulAutoLoads >= iasMax/);
assert.match(js, /AksrtFrontend\.rehydrate\(target, "ajax"\)/);
assert.match(js, /classList\.add\("lazyload"\)/);
assert.match(media, /thumb-dplayer-playing/);
assert.match(media, /data-post-thumb-slider/);
assert.match(media, /data-post-thumb-video/);
assert.match(media, /window\.Grade\(container/);
assert.match(media, /linear-gradient\(135deg/);
assert.match(media, /aksrtGradientState/);
assert.match(read('src/Core/ScriptsPart1c.php'), /'aksrt-grade'/);
});
test('the pinned Zibll baseline records all source checksums', () => {
const baseline = JSON.parse(read('tests/visual/zibll-9.0-article-baseline.json'));
assert.equal(baseline.theme, 'Zibll');
assert.equal(baseline.version, '9.0');
const files = Object.values(baseline.files);
assert.ok(files.length >= 6);
for (const file of files) {
assert.match(file.sha256, /^[a-f0-9]{64}$/);
}
});
test('article visual matrix pins every breakpoint, state and strict threshold', () => {
const matrix = JSON.parse(read('tests/visual/article-visual-matrix-v1.json'));
assert.deepEqual(matrix.viewports.map((item) => item.width), [1440, 1024, 768, 767, 641, 640, 390]);
assert.deepEqual(matrix.colorSchemes, ['light', 'dark']);
assert.deepEqual(matrix.layouts, ['sidebar', 'no-sidebar']);
assert.deepEqual(matrix.listModes, ['thumb', 'text', 'thumb_if_has', 'card']);
assert.deepEqual(matrix.styles, ['default', 'style2', 'style3']);
assert.ok(matrix.routes.some((route) => route.id === 'author-a'));
assert.ok(matrix.routes.some((route) => route.states.includes('infinite')));
assert.ok(matrix.postScenarios.includes('no-image'));
assert.ok(matrix.postScenarios.includes('multi'));
assert.ok(matrix.postScenarios.includes('video'));
assert.ok(matrix.postScenarios.includes('slides'));
assert.equal(matrix.acceptance.dimensionDeltaPxMax, 1);
assert.equal(matrix.acceptance.pixelDifferencePercentMax, 1.5);
});