Files
wordpress-theme-aksrt/tests/admin/post-page-settings-reference-contract.test.cjs
T

353 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 「文章页面」后台设置页契约(对齐参考主题 zibll 1:1):
* 1) PHP schema(SchemaPostList.php page section 2.0.0)字段键、顺序、默认值与
* JS schema(assets/admin/src/schema/sections/post-list/page.js)一致;
* 2) 参考主题字段全集存在:三类封面(article_image_cover / article_slide_cover /
* article_video_cover + option fieldset)、post_single_hide_time_s、
* post_p_indent_s、breadcrumbs_single_s、breadcrumbs_home_text(switcher)、
* post_prevnext_s、post_authordesc_s、article_maxheight_kg/maxheight、
* 一言三项、post_copyright_s/post_copyright、post_button_toptext、
* post_front_content/after_content、post_related_*;
* 3) 幻灯片 fieldset 全字段及嵌套依赖(direction/scale_height/auto_height 组合)
* 对齐参考主题 CFS_Module::slide;
* 4) 旧 AKSRT 私有/异名字段(阅读进度条、面包屑末项文本、作者卡片配置、收藏开关、
* share_s 与 post_rewards_*、旧封面键)已彻底移除,无双路兼容;
* 5) 运行时(PostDisplay::get_post_page_settings)只读取新键:幻灯片配置归一化并
* 接线 aksrt_render_slider,p_indent 接线容器类,breadcrumbs_home_text 为
* switcher 布尔语义(true = 用「首页」替代网站名称)。
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const php = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaPostList.php'), 'utf8');
const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/post-list/page.js'), 'utf8');
const runtime = fs.readFileSync(path.join(root, 'src/Settings/PostDisplay.php'), 'utf8');
const facadeBulk = fs.readFileSync(path.join(root, 'src/Settings/FacadeBulk.php'), 'utf8');
const metaBox = fs.readFileSync(path.join(root, 'src/Core/PostMetaBox.php'), 'utf8');
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-frontend-tags.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-single-render.php'), 'utf8');
const contentCss = fs.readFileSync(path.join(root, 'assets/css/frontend/single-post/content.css'), 'utf8');
const shareJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/global/share.js'), 'utf8');
const sharePhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/GlobalSchema.php'), 'utf8');
const featuresJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/features.js'), 'utf8');
const usersPhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
/** Extract a PHP section body by its section key (balanced brackets). */
function phpSection(src, sectionKey) {
const start = src.indexOf(`'${sectionKey}' => [`);
assert.notEqual(start, -1, `PHP schema has a ${sectionKey} section`);
const open = src.indexOf('[', start);
let depth = 0;
for (let i = open; i < src.length; i++) {
const ch = src[i];
if (ch === "'" || ch === '"') {
const quote = ch;
i++;
while (i < src.length) {
if (src[i] === '\\') { i += 2; continue; }
if (src[i] === quote) break;
i++;
}
continue;
}
if (ch === '[') depth++;
else if (ch === ']') {
depth--;
if (depth === 0) return src.slice(open, i + 1);
}
}
throw new Error(`unbalanced ${sectionKey} section`);
}
const pagePhp = phpSection(php, 'page');
// 参考主题 admin-options.php 4934–5249 的字段顺序(含 fieldset 父字段与子字段)。
const PAGE_FIELD_SEQUENCE = [
'article_image_cover',
'article_image_cover_option',
'scale', // article_image_cover_option.scale
'article_slide_cover',
'article_slide_cover_option',
// article_slide_cover_option(CFS_Module::slide 全字段)
'direction', 'loop', 'button', 'pagination', 'effect', 'scale_height', 'scale',
'auto_height', 'max_height', 'min_height', 'pc_height', 'm_height',
'spacebetween', 'speed', 'autoplay', 'interval',
'article_video_cover',
'article_video_cover_option',
'scale', // article_video_cover_option.scale
'post_single_hide_time_s',
'post_p_indent_s',
'breadcrumbs_single_s',
'breadcrumbs_home_text',
'post_prevnext_s',
'post_authordesc_s',
'article_maxheight_kg',
'article_maxheight',
'yiyan_single_content_header',
'yiyan_single_content_footer',
'yiyan_single_box',
'post_copyright_s',
'post_copyright',
'post_button_toptext',
'post_front_content',
'post_after_content',
'post_related_s',
'post_related_type',
'related_title',
'post_related_n',
'post_related_orderby',
];
// 旧 AKSRT 私有/异名字段:已迁移(share_s / post_rewards_*)或无参考对应项被移除。
const LEGACY_KEYS = [
'post_reading_progress',
'post_author_card_limit',
'post_author_card_orderby',
'post_author_card_style',
'post_favorite_enabled',
'post_wechat_share',
'post_article_image_cover',
'post_article_slide_cover',
'post_article_video_cover',
];
test('PHP page section 字段键与参考主题一致且顺序相同', () => {
// 字段定义均为 ['key' => '...', 'label' => ...] 开头;dependsOn 内的
// ['key' => '...', 'value'/'not' => ...] 不是字段定义,须排除。
const found = [];
const re = /'key'\s*=>\s*'([a-z0-9_]+)',\s*'label'/g;
let m;
while ((m = re.exec(pagePhp)) !== null) found.push(m[1]);
assert.deepEqual(found, PAGE_FIELD_SEQUENCE);
});
test('PHP page section 版本升级至 2.0.0 且为单一 post_page 组', () => {
assert.match(pagePhp, /'version'\s*=>\s*'2\.0\.0'/);
assert.match(pagePhp, /'groups'\s*=>\s*\[\s*\['key'\s*=>\s*'post_page',\s*'title'\s*=>\s*'文章页面'/);
});
test('PHP page section 默认值对齐参考主题', () => {
// 三类封面开关。
assert.match(pagePhp, /'article_image_cover',\s*'label'\s*=>\s*'图片封面',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
assert.match(pagePhp, /'article_slide_cover',\s*'label'\s*=>\s*'幻灯片封面',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
assert.match(pagePhp, /'article_video_cover',\s*'label'\s*=>\s*'视频封面',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
// 图片封面比例:20-200 step5,默认 35。
assert.match(pagePhp, /'scale',\s*'label'\s*=>\s*'图片长宽比例',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*35,\s*'min'\s*=>\s*20,\s*'max'\s*=>\s*200,\s*'step'\s*=>\s*5/);
// 视频封面比例:0-200,默认 0(为 0 则不固定长宽比例)。
assert.match(pagePhp, /'scale',\s*'label'\s*=>\s*'固定长宽比例',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*0,\s*'min'\s*=>\s*0,\s*'max'\s*=>\s*200/);
// 面包屑「首页」替代开关(switcher,默认 true)。
assert.match(pagePhp, /'breadcrumbs_home_text',\s*'label'\s*=>\s*'面包屑导航用“首页”替代网站名称',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
// 段落缩进 / 隐藏时间。
assert.match(pagePhp, /'post_p_indent_s',\s*'label'\s*=>\s*'内容段落缩进',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*false/);
assert.match(pagePhp, /'post_single_hide_time_s',\s*'label'\s*=>\s*'隐藏时间',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*false/);
// 内容高度限制:slider 600-3000 step100。
assert.match(pagePhp, /'article_maxheight_kg',\s*'label'\s*=>\s*'内容高度限制',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*false/);
assert.match(pagePhp, /'article_maxheight',\s*'label'\s*=>\s*'限制的最大高度',\s*'type'\s*=>\s*'slider',\s*'default'\s*=>\s*1000,\s*'min'\s*=>\s*600,\s*'max'\s*=>\s*3000,\s*'step'\s*=>\s*100/);
// 版权声明。
assert.match(pagePhp, /'post_copyright_s',\s*'label'\s*=>\s*'版权声明',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
assert.match(pagePhp, /'post_copyright',\s*'label'\s*=>\s*'版权提示内容',\s*'type'\s*=>\s*'textarea',\s*'default'\s*=>\s*'文章版权归作者所有,未经允许请勿转载。'/);
assert.match(pagePhp, /'post_button_toptext',\s*'label'\s*=>\s*'文章页脚文案',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'喜欢就支持一下吧'/);
// 相关文章:image_select 三样式 + 数量 4-12 step2 + 排序默认 views。
assert.match(pagePhp, /'post_related_type',\s*'label'\s*=>\s*'显示样式',\s*'type'\s*=>\s*'image_select',\s*'default'\s*=>\s*'img'/);
assert.match(pagePhp, /'post_related_n',\s*'label'\s*=>\s*'显示数量',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*6,\s*'min'\s*=>\s*4,\s*'max'\s*=>\s*12,\s*'step'\s*=>\s*2/);
assert.match(pagePhp, /'post_related_orderby',\s*'label'\s*=>\s*'排序方式',\s*'type'\s*=>\s*'select',\s*'default'\s*=>\s*'views'/);
});
test('PHP slide fieldset 全字段及嵌套依赖对齐 CFS_Module::slide', () => {
// 幻灯片默认值(pagination: true, pc_height: 380, m_height: 180 为参考主题默认)。
assert.match(pagePhp, /'direction'\s*=>\s*'horizontal',\s*'loop'\s*=>\s*true,\s*'button'\s*=>\s*false,\s*'pagination'\s*=>\s*true,\s*'effect'\s*=>\s*'slide'/);
assert.match(pagePhp, /'pc_height'\s*=>\s*380,\s*'m_height'\s*=>\s*180,\s*'spacebetween'\s*=>\s*15,\s*'speed'\s*=>\s*0,\s*'autoplay'\s*=>\s*true,\s*'interval'\s*=>\s*4/);
// 嵌套依赖:scale_height 需要 direction != vertical。
assert.match(pagePhp, /'scale_height',\s*'label'\s*=>\s*'按比例自动高度',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true,\s*'dependsOn'\s*=>\s*\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\]/);
// scale 需要 direction != vertical 且 scale_height 开启。
assert.match(pagePhp, /'scale',\s*'label'\s*=>\s*'长宽比例'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\],\s*\['key'\s*=>\s*'scale_height',\s*'value'\s*=>\s*true\]\]\]/);
// auto_height 需要 direction != vertical 且 scale_height 关闭。
assert.match(pagePhp, /'auto_height',\s*'label'\s*=>\s*'自动高度'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\],\s*\['key'\s*=>\s*'scale_height',\s*'value'\s*=>\s*false\]\]\]/);
// max/min_height 需要 auto_height 开启 + direction != vertical + scale_height 关闭。
assert.match(pagePhp, /'max_height',\s*'label'\s*=>\s*'最大高度'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'auto_height',\s*'value'\s*=>\s*true\],\s*\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\],\s*\['key'\s*=>\s*'scale_height',\s*'value'\s*=>\s*false\]\]\]/);
// interval 依赖 autoplay。
assert.match(pagePhp, /'interval',\s*'label'\s*=>\s*'停顿时间'[\s\S]*?'dependsOn'\s*=>\s*\['key'\s*=>\s*'autoplay',\s*'value'\s*=>\s*true\]/);
// 五种切换动画。
for (const effect of ['slide', 'fade', 'cube', 'coverflow', 'flip']) {
assert.ok(pagePhp.includes(`'value' => '${effect}'`), `缺少切换动画 ${effect}`);
}
});
test('PHP page section 不含旧私有/异名字段(无双路)', () => {
for (const legacy of LEGACY_KEYS) {
assert.ok(!pagePhp.includes(`'${legacy}'`), `page section 残留旧字段: ${legacy}`);
}
// share_s / post_rewards_* 已迁移至分享设置页与用户功能页。
assert.ok(!pagePhp.includes(`'share_s'`), 'page section 残留 share_s');
assert.ok(!pagePhp.includes(`'post_rewards_`), 'page section 残留 post_rewards_*');
});
test('JS schema 与 PHP 字段键一致(ui_only 一言行除外)', () => {
const keys = [];
const re = /key:\s*'([a-z0-9_]+)',\s*label:/g;
let m;
while ((m = re.exec(js)) !== null) keys.push(m[1]);
assert.deepEqual(keys, PAGE_FIELD_SEQUENCE);
for (const legacy of LEGACY_KEYS) {
assert.ok(!keys.includes(legacy), `JS schema 残留旧字段: ${legacy}`);
}
assert.ok(!js.includes(`key: 'share_s'`), 'JS page schema 残留 share_s');
assert.ok(!/key:\s*'post_rewards_/.test(js), 'JS page schema 残留 post_rewards_*');
// 参考主题视频封面配置教程链接以 ui_only description 承载。
assert.match(js, /ui_only:\s*true/);
assert.match(js, /https:\/\/www\.zibll\.com\/1900\.html/);
});
test('JS schema 默认值与依赖对齐参考主题', () => {
assert.match(js, /article_image_cover:\s*true/);
assert.match(js, /article_image_cover_option:\s*\{\s*scale:\s*35\s*\}/);
assert.match(js, /article_slide_cover:\s*true/);
assert.match(js, /article_video_cover:\s*true/);
assert.match(js, /article_video_cover_option:\s*\{\s*scale:\s*0\s*\}/);
assert.match(js, /post_single_hide_time_s:\s*false/);
assert.match(js, /post_p_indent_s:\s*false/);
assert.match(js, /breadcrumbs_single_s:\s*true/);
assert.match(js, /breadcrumbs_home_text:\s*true/);
assert.match(js, /post_prevnext_s:\s*true/);
assert.match(js, /post_authordesc_s:\s*true/);
assert.match(js, /article_maxheight_kg:\s*false/);
assert.match(js, /article_maxheight:\s*1000/);
assert.match(js, /post_copyright_s:\s*true/);
assert.match(js, /post_copyright:\s*'文章版权归作者所有,未经允许请勿转载。'/);
assert.match(js, /post_button_toptext:\s*'喜欢就支持一下吧'/);
assert.match(js, /post_related_s:\s*true/);
assert.match(js, /post_related_type:\s*'img'/);
assert.match(js, /related_title:\s*'相关推荐'/);
assert.match(js, /post_related_n:\s*6/);
assert.match(js, /post_related_orderby:\s*'views'/);
// 幻灯片嵌套依赖(dependsOn {key, not} / {all: [...]} 形式)。
assert.match(js, /key:\s*'scale_height',[\s\S]*?dependsOn:\s*\{\s*key:\s*'direction',\s*not:\s*'vertical'\s*\}/);
assert.match(js, /key:\s*'scale',[\s\S]*?dependsOn:\s*\{\s*all:\s*\[\{\s*key:\s*'direction',\s*not:\s*'vertical'\s*\},\s*\{\s*key:\s*'scale_height',\s*value:\s*true\s*\}\]\s*\}/);
assert.match(js, /key:\s*'auto_height',[\s\S]*?dependsOn:\s*\{\s*all:\s*\[\{\s*key:\s*'direction',\s*not:\s*'vertical'\s*\},\s*\{\s*key:\s*'scale_height',\s*value:\s*false\s*\}\]\s*\}/);
assert.match(js, /key:\s*'interval',[\s\S]*?dependsOn:\s*\{\s*key:\s*'autoplay',\s*value:\s*true\s*\}/);
});
test('社交分享位于全局功能并使用参考主题唯一键集', () => {
assert.match(sharePhp, /'share' => \[\s*'key'\s*=> 'share',\s*'title'\s*=> '社交分享'/);
assert.match(sharePhp, /'share_s', 'label' => '文章分享功能', 'type' => 'switcher', 'default' => true/);
assert.match(sharePhp, /'share_items', 'label' => '分享平台', 'type' => 'custom', 'renderer' => 'sorter', 'sanitize_type' => 'array'/);
assert.match(shareJs, /key: 'share_items',[\s\S]*?type: 'custom',[\s\S]*?renderer: 'sorter'/);
assert.match(sharePhp, /'wechat_share_s', 'label' => '微信分享有图'/);
assert.match(sharePhp, /'wechat_share_option', 'label' => '微信分享有图配置', 'type' => 'fieldset'/);
assert.match(sharePhp, /'share_img', 'label' => '生成海报分享', 'type' => 'switcher', 'default' => true/);
for (const key of ['share_img_compatible_s', 'share_img_byimg', 'share_logo', 'share_desc']) {
assert.ok(sharePhp.includes(`'${key}'`), `PHP 社交分享缺少 ${key}`);
assert.ok(shareJs.includes(`key: '${key}'`), `JS 社交分享缺少 ${key}`);
}
assert.match(shareJs, /key: 'global\.share'/);
assert.match(shareJs, /share_s: true/);
assert.doesNotMatch(sharePhp, /share_wechat_s|share_wechat_appid|share_wechat_app_secret|share_only_logo/);
assert.doesNotMatch(shareJs, /share_wechat_s|share_wechat_appid|share_wechat_app_secret|share_only_logo/);
});
test('post_rewards_* 迁移至用户功能页 rewards 组(对齐参考主题 8977 行)', () => {
assert.match(usersPhp, /\['key' => 'rewards', 'title' => '打赏', 'fields' => \[/);
assert.match(usersPhp, /'post_rewards_s', 'label' => '用户打赏功能', 'type' => 'switcher', 'default' => true/);
assert.match(usersPhp, /'post_rewards_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'/);
assert.match(featuresJs, /post_rewards_s:\s*true/);
assert.match(featuresJs, /post_rewards_text:\s*'赞赏'/);
assert.match(featuresJs, /key: 'post_rewards_s', label: '用户打赏功能', type: 'switch', default: true/);
});
test('旧字段在 schema 与运行时中均已移除(无双路)', () => {
for (const legacy of LEGACY_KEYS) {
assert.ok(!php.includes(legacy), `PHP schema 残留旧字段: ${legacy}`);
assert.ok(!runtime.includes(legacy), `PostDisplay 残留旧字段: ${legacy}`);
assert.ok(!js.includes(legacy), `page.js 残留旧字段: ${legacy}`);
assert.ok(!metaBox.includes(legacy), `PostMetaBox 残留旧字段: ${legacy}`);
}
// 面包屑末项正文无参考配置:schema 字段移除,运行时硬编码为常开。
assert.ok(!pagePhp.includes(`'breadcrumbs_single_text'`), 'page section 残留 breadcrumbs_single_text');
assert.ok(!js.includes(`key: 'breadcrumbs_single_text'`), 'page.js 残留 breadcrumbs_single_text');
assert.match(runtime, /'breadcrumbs_single_text'\s*=>\s*true/);
// aksrt-frontend-tags 不再有 breadcrumbs_home_text 文本兼容(switcher 布尔语义)。
assert.ok(!frontendTags.includes("breadcrumbs_home'] = (string) $settings['breadcrumbs_home_text'"), 'frontend-tags 残留 breadcrumbs_home_text 文本兼容');
});
test('运行时只读取新键并接线封面/缩进/版权/一言', () => {
// 三类封面 + option fieldset。
assert.match(runtime, /\$settings\['article_image_cover'\]/);
assert.match(runtime, /\$settings\['article_image_cover_option'\]/);
assert.match(runtime, /\$settings\['article_slide_cover'\]/);
assert.match(runtime, /\$settings\['article_slide_cover_option'\]/);
assert.match(runtime, /\$settings\['article_video_cover'\]/);
assert.match(runtime, /\$settings\['article_video_cover_option'\]/);
// 图片封面比例 clamp 20-200(参考主题默认 35)。
assert.match(runtime, /max\(20, min\(200, \(int\) \(\$image_cover_option\['scale'\] \?\? 35\)\)\)/);
// 视频封面比例 clamp 0-200(0 = 不固定长宽比例)。
assert.match(runtime, /max\(0, min\(200, \(int\) \(\$video_cover_option\['scale'\] \?\? 0\)\)\)/);
// 幻灯片配置归一化(CFS_Module::slide 全字段)。
assert.match(runtime, /normalize_slide_cover_option\(\$slide_option_raw\)/);
for (const key of ['direction', 'loop', 'button', 'pagination', 'effect', 'scale_height', 'scale', 'auto_height', 'max_height', 'min_height', 'pc_height', 'm_height', 'spacebetween', 'speed', 'autoplay', 'interval']) {
assert.ok(runtime.includes(`'${key}'`), `normalize_slide_cover_option 缺少 ${key}`);
}
// 面包屑「首页」替代语义:true → 「首页」,false → 网站名称。
assert.match(runtime, /!isset\(\$settings\['breadcrumbs_home_text'\]\) \|\| !empty\(\$settings\['breadcrumbs_home_text'\]\)/);
assert.match(runtime, /get_bloginfo\('name'\)/);
// 段落缩进 / 高度限制 / 版权 / 页脚文案 / 插入内容 / 一言 / 相关文章。
assert.match(runtime, /\$settings\['post_p_indent_s'\]/);
assert.match(runtime, /\$settings\['article_maxheight_kg'\]/);
assert.match(runtime, /\$settings\['article_maxheight'\]/);
assert.match(runtime, /\$settings\['post_copyright_s'\]/);
assert.match(runtime, /\$settings\['post_copyright'\]/);
assert.match(runtime, /\$settings\['post_button_toptext'\]/);
assert.match(runtime, /\$settings\['post_front_content'\]/);
assert.match(runtime, /\$settings\['post_after_content'\]/);
assert.match(runtime, /\$settings\['yiyan_single_content_header'\]/);
assert.match(runtime, /\$settings\['yiyan_single_content_footer'\]/);
assert.match(runtime, /\$settings\['yiyan_single_box'\]/);
assert.match(runtime, /\$settings\['post_related_s'\]/);
assert.match(runtime, /\$settings\['post_related_type'\]/);
assert.match(runtime, /\$settings\['post_related_n'\]/);
assert.match(runtime, /\$settings\['post_related_orderby'\]/);
// 阅读进度/收藏/作者卡片无参考配置:常量默认(无双路)。
assert.match(runtime, /'reading_progress'\s*=>\s*true/);
assert.match(runtime, /'favorite_enabled'\s*=>\s*true/);
assert.match(runtime, /'author_card_limit'\s*=>\s*6/);
});
test('前台链路:幻灯片配置接线渲染器,p_indent 接线容器与 CSS', () => {
// 幻灯片配置接入 aksrt_render_slider(effect/direction/spaceBetween/speed/interval)。
assert.match(singleRender, /article_slide_cover_option/);
assert.match(singleRender, /'effect'\s*=>\s*\(string\) \(\$slide_option\['effect'\] \?\? 'slide'\)/);
assert.match(singleRender, /'direction'\s*=>\s*\(string\) \(\$slide_option\['direction'\] \?\? 'horizontal'\)/);
assert.match(singleRender, /'spaceBetween'\s*=>\s*\(int\) \(\$slide_option\['spacebetween'\] \?\? 15\)/);
assert.match(singleRender, /'interval'\s*=>\s*max\(1, \(int\) \(\$slide_option\['interval'\] \?\? 4\)\) \* 1000/);
assert.match(singleRender, /'show_nav'\s*=>\s*!empty\(\$slide_option\['button'\]\)/);
assert.match(singleRender, /'show_pagination'\s*=>\s*!empty\(\$slide_option\['pagination'\]\)/);
// 固定高度模式(scale_height)与比例高度模式(scale + 50px 标题预留)双分支。
assert.match(singleRender, /!empty\(\$slide_option\['scale_height'\]\)/);
assert.match(singleRender, /\$slider_args\['pc_height'\] = \(\(int\) \(\$slide_option\['pc_height'\] \?\? 380\)\) \. 'px'/);
assert.match(singleRender, /\$slider_args\['scale_ratio'\] = max\(10, \(int\) \(\$slide_option\['scale'\] \?\? 35\)\) \/ 100/);
// 图片封面比例 clamp 20-200;视频封面 0 → 16:9(56.25)回退。
assert.match(singleRender, /max\(20, min\(200, \(int\) \$ctx\['img_scale'\]\)\)/);
assert.match(singleRender, /max\(0, min\(200, \(int\) \$ctx\['video_scale'\]\)\)/);
assert.match(singleRender, /\$video_scale > 0 \? \(string\) \$video_scale : '56\.25'/);
// p_indent 容器类 + CSS 规则(首行缩进 2em)。
assert.match(singleRender, /p-indent/);
assert.match(contentCss, /\.wp-posts-content\.p-indent p\s*\{\s*text-indent:\s*2em;/);
// 前端回退数组包含新键。
assert.match(frontendTags, /'p_indent'\s*=>\s*false/);
assert.match(frontendTags, /'article_image_cover_scale'\s*=>\s*35/);
assert.match(frontendTags, /'article_slide_cover_option'\s*=>\s*\[\]/);
assert.match(frontendTags, /'article_video_cover_scale'\s*=>\s*0/);
// PostMetaBox 读取新封面键(文章级封面字段可见性)。
assert.match(metaBox, /!empty\(\$settings\['article_image_cover'\]\)/);
assert.match(metaBox, /!empty\(\$settings\['article_slide_cover'\]\)/);
assert.match(metaBox, /!empty\(\$settings\['article_video_cover'\]\)/);
// FacadeBulk 独立回退与新键同步。
assert.match(facadeBulk, /'breadcrumbs_home_replace'/);
assert.match(facadeBulk, /'article_slide_cover_option'/);
});