const test = require('node:test'); const assert = require('node:assert/strict'); const { execFileSync } = require('node:child_process'); const fs = require('node:fs'); const path = require('node:path'); const root = path.resolve(__dirname, '../..'); const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); const cases = JSON.parse(execFileSync('php', [path.join(__dirname, 'tab-posts-render-harness.php')], { encoding: 'utf8' })).cases; function row(html, id) { const result = html.split('
').slice(1) .find((item) => item.includes(`href="https://example.test/post/${id}"`)); assert.ok(result, `the actual mini renderer emitted post ${id}`); return result; } function badge(html, id) { const result = row(html, id).match(/`); } assert.doesNotMatch(cases.pay_meta_right, /旧版资源标签/); assert.match(badge(cases.pay_meta_custom_currency, 70), /€<\/span>12\.5<\/item>/); assert.match(badge(cases.pay_meta_right, 55), /¥<\/span>0\.5<\/item>/); }); test('points mini badges use the existing SVG mark inside em09 without the long currency name', () => { for (const [id, amount] of [[62, 125], [61, 60], [60, 500], [59, 12]]) { const html = badge(cases.pay_meta_right, id); assert.ok(html.includes(`${amount}`)); assert.doesNotMatch(html, /积分|¥|€|class="icon"/); } }); test('zero prices render the short free badge and take priority over VIP purchase limits', () => { for (const [id, title] of [[66, '免费阅读'], [65, '免费资源'], [64, '免费图片'], [63, '免费视频']]) { const html = badge(cases.pay_meta_right, id); assert.ok(html.includes(`title="${title}"`)); assert.match(html, /<\/i>免费<\/item>$/); assert.doesNotMatch(html, /会员专属|jb-vip|em09|免费资源<\/item>/); } }); test('VIP badges use the selected level image inline and keep the reference membership-switch rule', () => { for (const html of [cases.pay_meta_right, cases.pay_vip1_only, cases.pay_vip2_only]) { for (const [id, level, name] of [[58, 1, '黄金会员'], [57, 2, '钻石会员']]) { const current = badge(html, id); assert.ok(current.includes(`mr6 jb-vip${level}"`)); assert.ok(current.includes(`${name}会员专属`)); assert.doesNotMatch(current, /fa-certificate|class="em09"/); } } for (const id of [58, 57]) { const html = badge(cases.pay_vip_disabled, id); assert.match(html, /mr6 c-yellow/); assert.match(html, /¥<\/span>12\.5/); assert.doesNotMatch(html, /会员专属|img-icon|jb-vip/); } }); test('mini badges stay on the correct meta side, disappear with meta and ignore non-paid posts', () => { const right = row(cases.pay_meta_right, 70); const left = row(cases.pay_meta_left, 70); assert.ok(right.indexOf('class="meta-right"') < right.indexOf('class="meta-pay ')); assert.ok(left.indexOf('class="meta-pay ') < left.indexOf('class="meta-right"')); badge(cases.pay_meta_right, 70); badge(cases.pay_meta_left, 70); assert.doesNotMatch(cases.pay_meta_hidden, /meta-pay|item-meta/); assert.doesNotMatch(row(cases.pay_meta_right, 51), /meta-pay/); assert.equal(cases.pay_empty, ''); }); test('mini badge styles preserve reference padding and VIP aspect ratio across page bundles', () => { const widgetCss = read('assets/widgets/frontend.css'); const rule = widgetCss.match(/\.widget-tab-post \.item-meta \.meta-pay, \.widget-main-post \.item-meta \.meta-pay\s*\{([^}]+)\}/)[1]; assert.match(rule, /padding:\s*0 4px/); assert.match(rule, /cursor:\s*auto/); for (const file of ['assets/css/frontend/post-card.css', 'assets/css/build/homepage.css']) { assert.doesNotMatch(read(file), /\.item-meta \.meta-pay img\s*\{/); } assert.match(read('assets/css/main.css'), /\.img-icon\s*\{[^}]*height:\s*1\.1em;[^}]*display:\s*inline-block/s); });