'use strict'; // 文章详情页顶部封面高度契约(对齐参考主题): // 单图/多图(轮播)封面标题叠加封面,高度 = calc(比例% + 50px)(zibll .single-cover / // .single-cover-slide);视频封面标题在下方 footer、无叠加,高度 = 裸比例% // (zibll .dplayer-scale-height)。 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/aksrt-single-render.php'); const sliderHelper = read('inc/helpers/aksrt-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 是标题叠加封面的补偿量,仅单图/多图封面适用;视频封面 // 对齐参考主题 .dplayer-scale-height,不得继承该公式。 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';/, '比例高度分支必须向 aksrt_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\('aksrt_render_slider'\)/, 'aksrt_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, /\.aksrt-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'); // 对齐 zibll:footer 里面包屑直接位于 flex jsb at 行,无自造包装层。 assert.match( singleRender, /single-video-footer dplayer-featured">\s*\n\s*
\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, /'aksrt-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(无标记生产方)已删除' ); });