The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding survived the sweeps in CSS classes, the SVG sprite prefix, a persisted widget field ID, JS events, admin Vue class names, test infrastructure and docblocks. Renames: - zib-widget / zib-widget-wrap / zib-slider → jio-widget-box / jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern widget shell, jio-box-* by the card primitives) - zib-partners-scroll + zps-* → jio-partners-scroll + jps-* - zib-combo-info-card + zci-* → jio-combo-info-card + jci-* - zibsvg- → jiosvg-, including the exported picker helpers and the fa-zibsvg- family-prefix guard - zibpay_type → pay_type; the dual-read fallback in PostQuery collapses to a single key - [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success → jio_ajax.success - aks-* admin Vue classes → jio-* - zib_palette() → palette() User-facing vendor traces are gone: the floating "more" button default no longer ships a zibll.com link or 子比主题 marketing copy, the notice widget ships no preset promo entries, and the four 官网教程 links are removed from the post-list admin schema. Test infrastructure renamed to neutral names (docker-compose.reference.yml, Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with their DB/volume/env/slug identifiers following. Kept intentionally, documented in tests/fixtures/README.md: the golden fixture reference fingerprint (a SHA-256 record of an external directory, read by validate-widget-golden / generate-widget-golden-fixture / audit-widget-system), the doesNotMatch guards that prove the old tokens are gone, and the reference theme's own meta key in the visual fixtures. No backward-compat or data migration: saved widget configs holding old icon keys or the old field ID silently lose them, per the project's rewrite-over-compatibility rule. Verified: 132/132 node contract tests, php tests/widget-system/run.php (146 assertions), audit-admin-contracts, audit-psr4-paths and validate-widget-golden all green; tracked bundles regenerated and in sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
233 lines
9.3 KiB
JavaScript
233 lines
9.3 KiB
JavaScript
'use strict';
|
||
|
||
// 文章详情页顶部封面高度契约(对齐参考主题):
|
||
// 单图/多图(轮播)封面标题叠加封面,高度 = calc(比例% + 50px)(.single-cover /
|
||
// .single-cover-slide);视频封面标题在下方 footer、无叠加,高度 = 裸比例%
|
||
// (视频封面高度计算契约)。
|
||
|
||
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 read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||
|
||
const singleRender = read('inc/helpers/jio-single-render.php');
|
||
const sliderHelper = read('inc/helpers/jio-slider.php');
|
||
|
||
test('single cover: image mode keeps the calc(scale% + 50px) height', () => {
|
||
assert.match(
|
||
singleRender,
|
||
/single-cover imgbox-container" style="padding-bottom:calc\(<\?php echo esc_attr\(\(string\) \$img_scale\); \?>% \+ 50px\);"/,
|
||
'单图封面必须输出 calc(img_scale% + 50px) 内联高度'
|
||
);
|
||
});
|
||
|
||
test('single cover: video mode uses the bare ratio height like the reference dplayer box', () => {
|
||
// 参考主题 article_video_cover_option.scale:0-200,0 = 不固定长宽比 → 16:9 回退。
|
||
assert.match(
|
||
singleRender,
|
||
/\$video_padding = \$video_scale > 0 \? \(string\) \$video_scale : '56\.25';/,
|
||
'视频封面 scale=0 必须回退 16:9(56.25%),非 0 输出裸比例'
|
||
);
|
||
assert.match(
|
||
singleRender,
|
||
/single-cover--video imgbox-container" style="padding-bottom:<\?php echo esc_attr\(\$video_padding\); \?>%;"/,
|
||
'视频封面必须输出裸比例高度(标题在下方 footer,无叠加补偿)'
|
||
);
|
||
// +50px 是标题叠加封面的补偿量,仅单图/多图封面适用;视频封面不得继承该公式。
|
||
assert.doesNotMatch(
|
||
singleRender,
|
||
/single-cover--video imgbox-container" style="padding-bottom:calc\(/,
|
||
'视频封面禁止输出 +50px 叠加补偿高度'
|
||
);
|
||
});
|
||
|
||
test('single cover: slide mode reserves the same +50px via the slider helper', () => {
|
||
// 比例高度模式(scale_height 关闭分支)向 slider helper 传 scale_extra=50px;
|
||
// 固定高度模式(scale_height 开启分支)改用参考主题 pc/m_height 定高。
|
||
assert.match(
|
||
singleRender,
|
||
/\$slider_args\['scale_extra'\] = '50px';/,
|
||
'比例高度分支必须向 jio_render_slider 传 scale_extra=50px'
|
||
);
|
||
assert.match(
|
||
singleRender,
|
||
/\$slider_args\['pc_height'\] = \(\(int\) \(\$slide_option\['pc_height'\] \?\? 380\)\) \. 'px';/,
|
||
'固定高度分支必须使用参考主题 pc_height 定高'
|
||
);
|
||
assert.match(
|
||
sliderHelper,
|
||
/'scale_extra'\s*=>\s*''/,
|
||
'slider helper 需声明 scale_extra 默认参数'
|
||
);
|
||
// wrapper 的 padding-bottom 必须由比例 + 附加量组合而成。
|
||
assert.match(
|
||
sliderHelper,
|
||
/\$scale_padding = 'calc\(' \. \$scale_padding \. ' \+ ' \. \$scale_extra \. '\)';/,
|
||
'scale 模式必须把 scale_extra 合成 calc 表达式'
|
||
);
|
||
});
|
||
|
||
test('single cover: the dead static slide fallback is gone', () => {
|
||
assert.doesNotMatch(
|
||
singleRender,
|
||
/function_exists\('jio_render_slider'\)/,
|
||
'jio_render_slider 由 bootstrap 无条件加载,不得保留 function_exists 双路判断'
|
||
);
|
||
assert.doesNotMatch(
|
||
singleRender,
|
||
/single-cover--slide imgbox-container" style="padding-bottom/,
|
||
'多图封面只允许经由 slider helper 输出,禁止保留静态 fallback 分支'
|
||
);
|
||
});
|
||
|
||
test('slider scale mode: completes the definite height chain for cover cropping', () => {
|
||
assert.doesNotMatch(
|
||
sliderHelper,
|
||
/--scale-ratio/,
|
||
'--scale-ratio 无任何 CSS 消费方,禁止输出'
|
||
);
|
||
// wrapper 决定盒子高度;pc/m-height 100% 让 .swiper-wrapper/.swiper-slide
|
||
// 铺满比例盒,使 object-fit: cover 在确定的盒内居中裁切(对齐单图封面)。
|
||
assert.match(
|
||
sliderHelper,
|
||
/\$height_style = '--pc-height:100%;--m-height:100%;';/
|
||
);
|
||
assert.match(
|
||
sliderHelper,
|
||
/esc_attr\(\$height_style \. \(\$args\['scale_height'\] \? 'position:absolute;inset:0;width:100%;height:100%;' : ''\)\)/,
|
||
'内层 .new-swiper 必须绝对定位铺满 wrapper'
|
||
);
|
||
});
|
||
|
||
test('video cover: the player video fills and center-crops like the image cover', () => {
|
||
const player = read('assets/js/video-player.js');
|
||
assert.match(
|
||
player,
|
||
/\.jio-video video\{display:block;width:100%;height:100%;object-fit:cover;\}/,
|
||
'视频必须铺满封面比例盒并居中裁切'
|
||
);
|
||
assert.doesNotMatch(
|
||
player,
|
||
/max-height:70vh/,
|
||
'封面盒高度由比例盒决定,不得再套 70vh 截断'
|
||
);
|
||
assert.doesNotMatch(
|
||
player,
|
||
/video\.style\.cssText/,
|
||
'视频不得携带内联样式:内联 object-fit 会以更高优先级覆盖注入样式表的 cover 契约,产生信箱式黑边'
|
||
);
|
||
});
|
||
|
||
test('video cover: play never runs against a sourceless player', () => {
|
||
const player = read('assets/js/video-player.js');
|
||
// toggle 必须先守卫无源状态,且 play() 的 promise 必须被捕获。
|
||
assert.match(player, /hasSource\s*=\s*function\s*\(\)\s*\{[^}]*currentSrc[^}]*\}/);
|
||
assert.match(player, /toggle\s*=\s*function\s*\(\)\s*\{\s*if\s*\(!this\.hasSource\(\)\)\s*return;/);
|
||
assert.match(player, /this\.video\.play\(\)\.catch\(function/);
|
||
// hls/flv CDN 脚本加载失败必须收敛,不得产生未处理 rejection。
|
||
assert.match(player, /loadScript\(HLS_SRC\)\.then\([\s\S]*?\}\)\.catch\(function/);
|
||
assert.match(player, /loadScript\(FLV_SRC\)\.then\([\s\S]*?\}\)\.catch\(function/);
|
||
// iframe 剧集归 runtime main 的 .switch-iframe 链路,播放器不得接管其选择器。
|
||
assert.doesNotMatch(player, /closest\('\.switch-video, \.switch-iframe'\)/);
|
||
assert.doesNotMatch(player, /querySelectorAll\('\.switch-video, \.switch-iframe'\)/);
|
||
});
|
||
|
||
test('video cover: footer breadcrumb sits in the reference flex row', () => {
|
||
const breadcrumbCss = read('assets/css/frontend/breadcrumb.css');
|
||
const coverCss = read('assets/css/frontend/single-post/cover.css');
|
||
|
||
// footer 里面包屑直接位于 flex jsb at 行,无自造包装层。
|
||
assert.match(
|
||
singleRender,
|
||
/single-video-footer dplayer-featured">\s*\n\s*<div class="flex jsb at">\s*\n\s*<\?php echo \$crumbs;/,
|
||
'视频 footer 面包屑必须在 flex jsb at 行内直出'
|
||
);
|
||
assert.doesNotMatch(
|
||
singleRender + breadcrumbCss + coverCss,
|
||
/post-breadcrumbs/,
|
||
'自造的 .post-breadcrumbs 包装层已死,禁止回归'
|
||
);
|
||
|
||
// 视频面包屑只归零 padding、保留基础弱化样式;-6px 左拉仅属封面叠加层。
|
||
assert.match(breadcrumbCss, /\.single-video \.breadcrumb \{\s*\n padding: 0;\s*\n\}/);
|
||
assert.doesNotMatch(
|
||
breadcrumbCss,
|
||
/\.single-video \.breadcrumb,[\s\S]{0,200}margin: 5px 0 0 -6px/,
|
||
'.single-video 不得并入 -6px 叠加组,否则面包屑贴出左边界'
|
||
);
|
||
|
||
// footer 内边距恢复 legacy 15px 契约,禁止 4px 侧边距覆盖。
|
||
assert.doesNotMatch(coverCss, /\.single-video-footer \{ padding:/);
|
||
});
|
||
|
||
test('resource singles get the cover leaf versioned, never via the aggregate @import', () => {
|
||
// 聚合文件的 @import 无版本号:浏览器会拿旧缓存(含已废弃的 .single-cover
|
||
// padding 规则)把封面高度链挤成 0,资源型文章封面直接空白。
|
||
assert.doesNotMatch(
|
||
read('assets/css/frontend/single-post.css'),
|
||
/@import[^;]*single-post\/cover\.css/,
|
||
'聚合样式不得再 @import cover leaf'
|
||
);
|
||
const enqueue = read('src/Core/ScriptsPart1b.php');
|
||
assert.match(
|
||
enqueue,
|
||
/if \(!empty\(\$singular_assets\['needs_cover'\]\)\) \{[\s\S]{0,400}?single-post\/cover\.css/,
|
||
'资源单页必须按 needs_cover 以 filemtime 版本号入队 cover leaf'
|
||
);
|
||
assert.match(
|
||
enqueue,
|
||
/'jio-single-post-cover'/,
|
||
'cover leaf 句柄与文章分支命名保持一致'
|
||
);
|
||
});
|
||
|
||
test('single cover css owns no fallback padding and the scale var is gone', () => {
|
||
for (const file of [
|
||
'assets/css/frontend/single-post/cover.css',
|
||
'assets/css/main.css',
|
||
'assets/css/main.min.css',
|
||
'assets/css/core/tokens.css',
|
||
'assets/css/build/public.css',
|
||
'assets/css/build/public.min.css',
|
||
]) {
|
||
const css = read(file);
|
||
assert.doesNotMatch(
|
||
css,
|
||
/\.single-cover\s*\{[^}]*padding-bottom/,
|
||
`${file} 的 .single-cover 不得声明 padding-bottom(高度归内联样式)`
|
||
);
|
||
assert.doesNotMatch(
|
||
css,
|
||
/--single-cover-scale/,
|
||
`${file} 不得再包含孤儿变量 --single-cover-scale`
|
||
);
|
||
}
|
||
assert.doesNotMatch(
|
||
read('src/Frontend/InlineStyleBuilder.php'),
|
||
/--single-cover-scale/,
|
||
'运行时 CSS 变量不得再输出孤儿 --single-cover-scale'
|
||
);
|
||
});
|
||
|
||
test('dead cover css is removed from every stylesheet surface', () => {
|
||
for (const file of [
|
||
'assets/css/frontend/single-post/cover.css',
|
||
'assets/css/main.css',
|
||
'assets/css/main.min.css',
|
||
'assets/css/build/public.css',
|
||
'assets/css/build/public.min.css',
|
||
]) {
|
||
const css = read(file);
|
||
assert.doesNotMatch(css, /\.single-cover-slide[,{ ]/, `${file} 不得再包含死的 .single-cover-slide 规则`);
|
||
assert.doesNotMatch(css, /\.single-cover-slider\b/, `${file} 不得再包含死的 .single-cover-slider 规则`);
|
||
}
|
||
assert.doesNotMatch(
|
||
read('assets/js/core/misc.js'),
|
||
/initSlideCovers/,
|
||
'misc.js 的 initSlideCovers(无标记生产方)已删除'
|
||
);
|
||
});
|