Files
wordpress-theme-aksrt/tests/widget-system/vip-modal-copy-fields.test.cjs
T
kate522 2e2558e5ce fix(vip): render 开通会员更多内容 as HTML in the modal
Three layers were dropping the markup: the save path was already relaxed to wp_kses_post via schema sanitize_type html, but the runtime output re-filtered more through the narrow equity whitelist (h/img/hr/table stripped), and pay-vip.js inserted it with node() whose textContent showed the tags as literal text. The card-pass desc had the same output-side strip despite its save-side fix.

- PaymentRuntime: more and pay_vip_pass_charge_desc now output via wp_kses_post, matching the save-time level (repo precedent: revenue note, after-sale desc)

- pay-vip.js: new htmlNode() helper (innerHTML) for server-sanitized rich text; more and cardPass.desc use it, plain-text fields keep node()

Verification: 3 new contract tests cover output-level kses and innerHTML rendering; node --test tests/**/*.test.cjs 640/640 pass; php -l and node --check clean.
2026-10-18 14:53:42 +08:00

175 lines
12 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.
/**
* VIP 后台设置弹窗文案字段契约(对照参考主题 zibll 修正)。
*
* 硬约束:
* 1) 弹窗文案字段更名为「会员介绍」(pay_user_vip_desc, input) 与
* 「开通会员更多内容」(pay_user_vip_more, textarea rows 4 +
* sanitize_type html),并从「会员等级」组移到分区末尾的
* 「界面文案」(modal-copy) 独立分组;
* 2) 文案默认值对齐 zibll(黄金会员/钻石会员、尊享专属特权、权益四项、
* 会员介绍与开通会员更多内容默认值),SPA schema / PHP schema /
* PaymentRuntime 回退三处一致——Settings::get_all() 不合并
* schema 默认值,运行时回退即实际默认值;
* 3) vip_renew_desc / vip_upgrade_desc 为死配置(AKSRT 无续费/升级
* 卡渲染面,全仓零消费方),两份 schema 与 PaymentRuntime 均已删除;
* 4) 权益与卡密说明字段保存时保留 HTML(sanitize_type html),
* 不再被 sanitize_textarea_field 剥掉标签。
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..', '..');
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
const rechargeJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js');
const vipTabPhp = read('inc', 'helpers', 'user-center-renderers', 'vip-tab.php');
const modalSource = read('src', 'Content', 'VipServiceModal.php');
const payVipJs = read('assets', 'js', 'pay-vip.js');
const DESC_DEFAULT = '开通VIP会员,享受会员专属折扣以及多项特权';
const MORE_DEFAULT = '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>';
const EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
// 取两个标记之间的源码段(分组切片,避免断言误伤其它功能的同名键)。
function slice(source, from, to) {
const start = source.indexOf(from);
assert.notStrictEqual(start, -1, `missing ${from}`);
const end = source.indexOf(to, start + from.length);
return source.slice(start, end === -1 ? undefined : end);
}
test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为正确类型', () => {
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp]]) {
assert.ok(!src.includes('弹窗顶部会员说明') && !src.includes('弹窗底部补充说明 HTML'), `${name} 旧名称应已移除`);
assert.ok(src.includes('会员介绍') && src.includes('开通会员更多内容'), `${name} 应含新名称`);
}
// 会员介绍为单行 input(zibll text「一句话简介」);开通会员更多内容为 rows 4 的 HTML textarea。
assert.match(
vipSchemaJs,
/key: 'pay_user_vip_desc',\s*\n\s*label: '会员介绍',\s*\n\s*type: 'input',\s*\n\s*default: '开通VIP会员,享受会员专属折扣以及多项特权'/
);
assert.match(
vipSchemaJs,
/key: 'pay_user_vip_more',\s*\n\s*label: '开通会员更多内容',\s*\n\s*type: 'textarea',\s*\n\s*sanitize_type: 'html'[\s\S]*?rows: 4/
);
assert.match(
schemaPhp,
/'key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权'/
);
assert.match(
schemaPhp,
/'key' => 'pay_user_vip_more', 'label' => '开通会员更多内容', 'type' => 'textarea', 'sanitize_type' => 'html'[\s\S]*?'rows' => 4/
);
// 帮助文本对齐 zibll 的 desc。
assert.ok(vipSchemaJs.includes('显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍'));
assert.ok(vipSchemaJs.includes('显示在开通界面底部位置,可以为提醒事项、用户协议等,支持HTML代码'));
});
test('两个文案字段位于分区末尾的界面文案分组且不再属于会员等级组', () => {
const modalJs = vipSchemaJs.indexOf("key: 'modal-copy'");
const modalPhp = schemaPhp.indexOf("'key' => 'modal-copy'");
assert.notStrictEqual(modalJs, -1, 'vip.js 应含 modal-copy 分组');
assert.notStrictEqual(modalPhp, -1, 'SchemaPayment.php 应含 modal-copy 分组');
assert.ok(modalJs > vipSchemaJs.indexOf("key: 'points-exchange'"), 'vip.js modal-copy 应在分区末尾');
assert.ok(modalPhp > schemaPhp.indexOf("'key' => 'points-exchange'"), 'SchemaPayment.php modal-copy 应在分区末尾');
// modal-copy 之后不再出现其它分组/字段 key。
// modal-copy 之后(至分区末尾)不再出现其它分组/字段 key;dependsOn 引用的等级开关除外。
assert.doesNotMatch(vipSchemaJs.slice(modalJs), /key: '(?!modal-copy|pay_user_vip_desc|pay_user_vip_more|pay_user_vip_1_s|pay_user_vip_2_s)/);
const phpTail = slice(schemaPhp, "'key' => 'modal-copy'", "'revenue'");
assert.doesNotMatch(phpTail, /'key' => '(?!modal-copy|pay_user_vip_desc|pay_user_vip_more|pay_user_vip_1_s|pay_user_vip_2_s)/);
// 「会员等级」组不再包含这两个字段。
assert.doesNotMatch(slice(vipSchemaJs, "key: 'basic'", "key: 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/);
assert.doesNotMatch(slice(schemaPhp, "'key' => 'basic'", "'key' => 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/);
// 依赖:任一等级启用时显示(zibll pay_user_vip_1_s 依赖的双等级适配)。
assert.ok(
vipSchemaJs.includes("dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }"),
'vip.js 应为 any 双等级依赖'
);
assert.ok(
schemaPhp.includes("'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]"),
'SchemaPayment.php 应为 any 双等级依赖'
);
});
test('续费/升级说明死配置已从两份 schema 与运行时删除', () => {
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) {
assert.ok(!src.includes('vip_renew_desc'), `${name} 不应再含 vip_renew_desc`);
assert.ok(!src.includes('vip_upgrade_desc'), `${name} 不应再含 vip_upgrade_desc`);
}
});
test('文案默认值三处一致并对齐 zibll', () => {
// SPA defaults 块与字段级 default。
assert.ok(vipSchemaJs.includes("pay_user_vip_1_name: '黄金会员'"));
assert.ok(vipSchemaJs.includes("pay_user_vip_2_name: '钻石会员'"));
assert.ok(vipSchemaJs.includes("pay_user_vip_1_desc: '尊享专属特权'"));
assert.ok(vipSchemaJs.includes("pay_user_vip_2_desc: '尊享专属特权'"));
assert.ok(vipSchemaJs.includes(`pay_user_vip_desc: '${DESC_DEFAULT}'`));
assert.ok(vipSchemaJs.includes(`pay_user_vip_more: '${MORE_DEFAULT}'`));
assert.ok(vipSchemaJs.includes(`default: '${EQUITY_DEFAULT}'`));
// PHP schema 字段 default。
assert.ok(schemaPhp.includes("'default' => '黄金会员'"));
assert.ok(schemaPhp.includes("'default' => '钻石会员'"));
assert.ok(schemaPhp.includes(`'default' => '${DESC_DEFAULT}'`));
assert.ok(schemaPhp.includes(`'default' => '${MORE_DEFAULT}'`));
// PaymentRuntime 运行时回退(get_all 不合并 schema 默认值,回退即实际默认值)。
assert.ok(paymentRuntime.includes(`$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('${DESC_DEFAULT}', 'aksrt')))`));
assert.ok(paymentRuntime.includes(`$more = (string) ($settings['pay_user_vip_more'] ?? '${MORE_DEFAULT}')`));
assert.ok(paymentRuntime.includes("$name1 = __('黄金会员', 'aksrt');"));
assert.ok(paymentRuntime.includes("$name2 = __('钻石会员', 'aksrt');"));
assert.ok(paymentRuntime.includes("$settings['pay_user_vip_1_desc'] ?? __('尊享专属特权', 'aksrt')"));
assert.ok(paymentRuntime.includes("$settings['pay_user_vip_2_desc'] ?? __('尊享专属特权', 'aksrt')"));
assert.ok(paymentRuntime.includes(EQUITY_DEFAULT));
// 旧默认值不再出现在任何一处。
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) {
assert.ok(!/尊享一级会员特权|尊享二级会员特权|免费下载部分资源|更高免费下载额度/.test(src), `${name} 旧默认值应已移除`);
}
});
test('权益与卡密说明字段保存时保留 HTML', () => {
assert.ok(vipSchemaJs.includes("key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
assert.ok(vipSchemaJs.includes("key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
assert.ok(schemaPhp.includes("'key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
assert.ok(schemaPhp.includes("'key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
assert.ok(rechargeJs.includes("key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html'"));
assert.ok(schemaPhp.includes("'key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html'"));
});
test('会员介绍运行时按单行文本净化', () => {
// 只收敛 VIP 设置函数:充值等其它设置的 $desc 与本文无关。
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
assert.ok(vipSettings.includes("$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc']"));
assert.ok(!vipSettings.includes('$desc = sanitize_textarea_field'));
});
test('开通会员更多内容只在弹窗内渲染,不裸露在用户中心页面', () => {
// 参考主题 zibpay_user_page_tab_content_vip 只渲染 pay_user_vip_desc;
// pay_user_vip_more 仅出现在两个弹窗函数(购买/续费弹窗)中。
assert.ok(!vipTabPhp.includes("vip_settings['more']"), '用户中心 VIP 页不应渲染 more');
assert.ok(!vipTabPhp.includes('muted-box padding-10 mini-radius mt15'), '用户中心 VIP 页不应有 more 提示盒');
// 弹窗侧保持:视图模型透传 more + pay-vip.js 右列渲染。
assert.match(modalSource, /'more'\s+=>/);
assert.match(payVipJs, /catalog\.more/);
});
test('more 与卡密说明输出按 post 级 HTML 放行', () => {
// 保存端已 wp_kses_post,输出端不能再窄白名单/textarea 净化二次剥标签。
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
assert.match(vipSettings, /'more'\s+=> wp_kses_post\(\$more\)/);
assert.doesNotMatch(vipSettings, /\$allowed_equity \+ \['div'/);
const rechargeSettings = slice(paymentRuntime, 'public static function get_payment_recharge_settings', 'public static function get_payment_vip_settings');
assert.match(rechargeSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_vip_pass_charge_desc'\]/);
});
test('弹窗内 more 与卡密说明按 HTML 渲染而非纯文本', () => {
// node() 走 textContent 会把标签显示为字面文本;富文本字段必须走 innerHTML。
assert.match(payVipJs, /function htmlNode\(tag, className, value\) \{[\s\S]*?el\.innerHTML = text\(value\);/);
assert.ok(payVipJs.includes('htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more)'));
assert.ok(payVipJs.includes('htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc)'));
});