Files
wordpress-theme-aksrt/tests/widget-system/referral-qr-contract.test.cjs
T
kate522andClaude Opus 5 81b60eb427 fix(revenue): replicate the reference rebate poster canvas layers
The poster modal shell already matched the reference theme, but the canvas
drawing diverged from zibll's poster-share.js rebate branch in four places:

- No 推荐人 tags layer. The reference paints a red #fc4e1f 24px line at
  img_height+160; AKSRT dropped the layer entirely, so the poster lost the
  referrer attribution that identifies whose link it is.
- The logo slot painted text. The reference contains the real logo image
  into (80, height-190, 300, 100); AKSRT drew the site name as a 30px string
  plus a hardcoded 长按识别二维码访问 caption in that slot.
- Bottom copy was hardcoded. The reference reads share_desc; now sourced
  from the new pay_rebate_poster_desc_bottom field with share_desc fallback.
- No banner fallback. The reference walks banner → banner_default →
  banner_spare; a single broken URL left the top half of the poster black.

Also aligns the week label to the reference's 周 prefix (was 星期日) and ports
drawTextOverflow for wrapping/truncation, which retires the local wrapText.

Adds pay_rebate_poster_banner and pay_rebate_poster_desc_bottom to both the
Vue and PHP schemas; rest_me now returns logo, compatible and referrer_name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-21 21:07:48 +08:00

249 lines
15 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.
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const root = path.resolve(__dirname, '..', '..')
const read = (...segments) => fs.readFileSync(path.join(root, ...segments), 'utf8')
test('QR generator is bundled locally; no external QR CDN in front-end sources', () => {
const vendor = path.join(root, 'assets', 'js', 'vendor', 'qrcode.min.js')
assert.ok(fs.existsSync(vendor), 'assets/js/vendor/qrcode.min.js must be committed')
const script = fs.readFileSync(vendor, 'utf8')
assert.ok(script.length > 10000, 'qrcode.min.js must not be a placeholder')
// 前端引用面不允许出现外部二维码 CDN。
const sources = [
read('assets', 'js', 'credits.js'),
read('inc', 'helpers', 'user-center-renderers', 'profile-revenue.php'),
read('src', 'Core', 'ScriptsPart1b.php'),
].join('\n')
assert.doesNotMatch(sources, /cdnjs\.cloudflare\.com[^"']*qrcode|cdn\.jsdelivr\.net[^"']*qrcode|unpkg\.com[^"']*qrcode/)
})
test('revenue detail card is a four-row info list with no standalone QR block', () => {
const template = read('inc', 'helpers', 'user-center-renderers', 'profile-revenue.php')
// 待偿欠款不在 UI 暴露:欠款机制(挂账/偿还)照常运行,只是不渲染。
// 参考主题的佣金卡片也没有这个概念,故不作为平价目标。
assert.doesNotMatch(template, /待偿欠款/)
assert.doesNotMatch(template, /uc-commission-card__debt/)
assert.doesNotMatch(template, /\$debt_amount/)
const css = read('assets', 'css', 'routes', 'user-center', 'revenue-earnings.css')
assert.doesNotMatch(css, /uc-commission-card__debt/)
// overview() 仍返回 debt(偿还逻辑依赖),fallback 形状必须与之一致。
assert.match(template, /'debt' => 0/)
const ledger = read('src', 'Payment', 'CommissionLedger.php')
assert.match(ledger, /'debt'\s*=>/)
// 二维码不再作为独立行:整行 markup 已删除。
assert.doesNotMatch(template, /data-affiliate-qr-row/)
assert.doesNotMatch(template, /data-affiliate-qr(?!-)/)
assert.doesNotMatch(template, /data-affiliate-qr-download/)
assert.doesNotMatch(template, /data-affiliate-qr-feedback/)
assert.doesNotMatch(template, /<canvas/)
// 推广海报按钮并入推广链接行([Url][复制链接][推广海报])。
assert.match(template, /uc-revenue-summary-row--link/)
assert.match(template, /data-affiliate-link/)
assert.match(template, /data-affiliate-copy/)
assert.match(template, /data-affiliate-poster/)
// 复制反馈走 Toast(对齐参考主题),不再有内联反馈元素。
assert.doesNotMatch(template, /data-affiliate-feedback/)
// 四行固定标签:推广链接 / 佣金比例 / 累计佣金 / 返佣详解。
for (const label of ['推广链接', '佣金比例', '累计佣金']) {
assert.match(template, new RegExp('<span><\\?php [a-z_]+\\([^)]*' + label), 'detail row label ' + label)
}
// 返佣详解标题走可配置字段,不再硬编码。
assert.match(template, /esc_html\(\$details_title\)/)
const script = read('assets', 'js', 'credits.js')
// 独立二维码绘制函数已删除;window.qrcode 仅供海报使用。
assert.doesNotMatch(script, /function renderAffiliateQr/)
assert.doesNotMatch(script, /renderAffiliateQr\(/)
assert.doesNotMatch(script, /data-affiliate-qr/)
// 海报弹窗:参考主题 poster-share 形态 —— Bootstrap 语义 + .poster-imgbox 舞台。
assert.match(script, /data-affiliate-poster-stage/)
assert.match(script, /data-affiliate-poster-download/)
assert.match(script, /modal-backdrop/)
assert.match(script, /classList\.add\('in'\)/)
assert.match(script, /poster-imgbox/)
assert.match(script, /classList\.add\('hide'\)/)
assert.match(script, /link\.download = 'referral-poster\.png'/)
// 弹窗开关不再走 uc-modal 的 openModal/closeModal(那套留给提现/转账弹窗)。
const posterInit = script.slice(script.indexOf('function initAffiliatePoster'), script.indexOf('function buildPosterCanvas'))
assert.doesNotMatch(posterInit, /openModal\(modal\)/)
assert.doesNotMatch(posterInit, /closeModal\(modal\)/)
assert.doesNotMatch(posterInit, /data-affiliate-poster-close/)
// 链接由 PHP 直出:复制按钮立即可用,REST 只负责海报配置与佣金比例。
assert.match(script, /var url = input \? input\.value : '';/)
assert.match(script, /if \(copy\) copy\.disabled = !url;/)
assert.doesNotMatch(script, /input\.value = j\.url/)
// 复制反馈走 Toast(notyf),不再写内联文本。
assert.match(script, /notyf\('推广链接已复制'/)
assert.doesNotMatch(script, /data-affiliate-feedback/)
})
test('promotion link is server-rendered so it needs no round trip', () => {
const template = read('inc', 'helpers', 'user-center-renderers', 'profile-revenue.php')
// input 带服务端 value,不依赖 JS 回填。
assert.match(template, /data-affiliate-link value="<\?php echo esc_attr\(\$affiliate_link_url\); \?>"/)
assert.match(template, /create_link\(\$user_id\)\['url'\]/)
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
// rest_me 不再做 COUNT 聚合,也不触发结算。
assert.doesNotMatch(attribution, /\$this->stats\(\$user_id\)/)
assert.doesNotMatch(attribution, /lazy_settle_due\(\)/)
})
test('poster canvas is built from admin-configured copy and gated on the poster switch', () => {
const runtime = read('src', 'Settings', 'PaymentRuntime.php')
assert.match(runtime, /'poster' => \[/)
assert.match(runtime, /pay_rebate_poster_s/)
assert.match(runtime, /pay_rebate_poster_img/)
assert.match(runtime, /pay_rebate_poster_title/)
assert.match(runtime, /pay_rebate_poster_desc/)
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
// 推广海报 payload:开关 / 主图 / 标题 / 简介 / 两级备用横幅 / 底部文案。
assert.match(attribution, /\$poster = \(array\) \(\$affiliate\['poster'\] \?\? \['enabled' => false\]\)/)
assert.match(attribution, /'poster' => \$poster/)
// 参考主题 rebate 的 logo 与底部文案取自分享设置,不在推广字段里重复配置。
assert.match(attribution, /\$poster\['logo'\] = esc_url_raw/)
assert.match(attribution, /\$poster\['compatible'\] = !empty\(\$share\['compatible_s'\]\)/)
assert.match(attribution, /\$poster\['bottom'\] = sanitize_text_field/)
// 参考主题 tags 层:`推荐人: {昵称}`。
assert.match(attribution, /'referrer_name' => \$user instanceof WP_User \? \$user->display_name/)
assert.match(attribution, /'site_name' => get_bloginfo\('name'\)/)
const script = read('assets', 'js', 'credits.js')
// 海报仅在开关开启时暴露按钮;推荐人昵称也由 REST 下发。
assert.match(script, /initAffiliatePoster\(root, url \|\| j\.url \|\| '', j\.poster \|\| \{\}, j\.site_name \|\| document\.title, j\.referrer_name \|\| ''\)/)
assert.match(script, /poster\.enabled !== true\) return/)
assert.match(script, /function buildPosterCanvas/)
assert.match(script, /image\.crossOrigin = 'anonymous'/)
// 顶部主图三级回退,对齐参考主题 banner → banner_default → banner_spare。
const posterRuntime = read('src', 'Settings', 'PaymentRuntime.php')
assert.match(posterRuntime, /'banner_default' => esc_url_raw\(\(string\) \(\$settings\['pay_rebate_poster_banner'\] \?\? ''\)\)/)
assert.match(posterRuntime, /'banner_spare' => get_template_directory_uri\(\) \. '\/assets\/img\/share_img\.jpg'/)
assert.match(posterRuntime, /'bottom' => sanitize_text_field\(\(string\) \(\$settings\['pay_rebate_poster_desc_bottom'\] \?\? ''\)\)/)
// 备用图必须是真实存在的资源,否则回退到 404 等于没回退。
assert.ok(fs.existsSync(path.join(root, 'assets', 'img', 'share_img.jpg')), 'assets/img/share_img.jpg must be committed')
// 新增的后台字段必须在 SPA schema 与 PHP schema 双侧注册,否则 contract audit 会红。
const vue = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'affiliate.js')
const php = read('src', 'Admin', 'Schema', 'SchemaPayment.php')
for (const key of ['pay_rebate_poster_banner', 'pay_rebate_poster_desc_bottom']) {
assert.match(vue, new RegExp("key: '" + key + "'"), 'vue schema ' + key)
assert.match(php, new RegExp("'key' => '" + key + "'"), 'php schema ' + key)
}
})
test('poster canvas layers match the reference rebate branch 1:1', () => {
const script = read('assets', 'js', 'credits.js')
const body = script.slice(script.indexOf('function buildPosterCanvas'), script.indexOf('// cover 裁切'))
// 参考主题 rebate 分支用 drawTextOverflow 的换行/截断,不是自研 wrapText。
assert.match(script, /function drawTextOverflow\(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y\)/)
assert.doesNotMatch(script, /function wrapText\(/, 'wrapText is dead once drawTextOverflow lands')
assert.doesNotMatch(script, /wrapText\(/)
// logo 用 contain 裁切画真实图片,不再把站名当文本画在 logo 位。
assert.match(script, /function containRect\(sx, sy, boxW, boxH, srcW, srcH\)/)
assert.match(body, /containRect\(80, height - 190, 300, 100, logo\.width, logo\.height\)/)
assert.doesNotMatch(body, /长按识别二维码访问/, 'hardcoded bottom copy must come from config')
assert.doesNotMatch(body, /ctx\.fillText\(String\(siteName/)
assert.match(body, /poster\.bottom \|\| '扫描二维码阅读全文'/)
// 推荐人标签层:参考主题 img_height+172 的红色 24px 层。
assert.match(body, /'推荐人: ' \+ String\(referrerName\)/)
assert.match(body, /drawTextOverflow\(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight \+ 162\)/)
// 站点名走「周」前缀(参考主题 zib__('week_prefix'))。
assert.match(body, /'周' \+ '日一二三四五六'\.charAt\(new Date\(\)\.getDay\(\)\)/)
assert.doesNotMatch(body, /\['星期日', '星期一'/)
// 层叠坐标必须与参考主题 rebate 分支一致:$title 于 (20, img+40)、
// $tags 于 (0, img+160)、$content 于 (0, img+220);子画布左对齐 x=40、基线偏移 2,
// drawTextOverflow 内部 +38,故 x 参数传 2。
assert.match(body, /drawTextOverflow\(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight \+ 42\)/)
assert.match(body, /drawTextOverflow\(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight \+ 162\)/)
assert.match(body, /drawTextOverflow\(ctx, description, width, 4, '25px Arial', 'rgba\(88,88,88,1\)', 39, 2, imgHeight \+ 222\)/)
// 底部文案基线:参考主题 $description 子画布置于 height-60,内部基线偏移 2。
assert.match(body, /height - 58/)
})
test('poster modal markup is a 1:1 replica of the reference poster-share dialog', () => {
const modal = read('inc', 'helpers', 'user-center-renderers', 'affiliate-poster-modal.php')
// 参考主题骨架:Bootstrap 模态壳 + 360px 无边框对话框 + hover 浮钮层。
assert.match(modal, /class="modal fade flex jc"/)
assert.match(modal, /class="modal-dialog poster-share hover-show"/)
assert.match(modal, /class="modal-content" style="height:220px;overflow:hidden"/)
assert.match(modal, /class="poster-imgbox hover-show" data-affiliate-poster-stage/)
assert.match(modal, /class="flex jc loading-mask absolute main-bg radius8"/)
assert.match(modal, /class="but cir jb-blue poster-download hide"/)
assert.match(modal, /class="but cir jb-yellow ml10"/)
assert.match(modal, /#icon-close/)
assert.match(modal, /data-dismiss="modal"/)
// 自研 uc-modal 卡片形态(白底 + 标题栏 + 整宽保存钮 + 浅灰画框)已退役。
assert.doesNotMatch(modal, /uc-modal__dialog/)
assert.doesNotMatch(modal, /uc-modal__title/)
assert.doesNotMatch(modal, /uc-modal__close/)
assert.doesNotMatch(modal, /uc-modal__overlay/)
assert.doesNotMatch(modal, /uc-affiliate-poster/)
assert.doesNotMatch(modal, /btn-block/)
// 配套的 .uc-affiliate-poster 画框规则已从样式表删除。
const css = read('assets', 'css', 'routes', 'user-center', 'revenue-earnings.css')
assert.doesNotMatch(css, /uc-affiliate-poster/)
})
test('poster modal shell is hidden at rest so it cannot block the whole page', () => {
const modal = read('inc', 'helpers', 'user-center-renderers', 'affiliate-poster-modal.php')
const script = read('assets', 'js', 'credits.js')
const css = read('assets', 'css', 'routes', 'user-center', 'credits.css')
// 标记、JS 查询与 CSS 钩子必须共用同一个属性,缺一即失效:
// 少了标记上的属性,JS 拿不到弹窗、CSS 也隐藏不了它。
assert.match(modal, /data-aksrt-affiliate-poster-modal/)
assert.match(script, /querySelector\('\[data-aksrt-affiliate-poster-modal\]'\)/)
// 服务端静态输出的 .modal.flex 必须显式隐藏。main.css 的 .flex{display:flex}
// 后加载且与 bootstrap 的 .modal{display:none} 同特异性,不在此回位就会让
// 静止态弹窗铺满全屏,叠加 .fade 的 opacity:0 形成隐形遮罩、拦截整页点击。
assert.match(css, /\.modal\[data-aksrt-affiliate-poster-modal\]\s*\{\s*display:\s*none;/)
assert.match(css, /\.modal\[data-aksrt-affiliate-poster-modal\]\.in\s*\{\s*display:\s*flex;/)
// 圆形按钮必须回位成 pill。credits.css 的 .uc-content .but.jb-blue 是 (0,3,0)
// 且后加载,会盖掉 main.css 的 .poster-share .but.cir,把蓝色下载钮压成方角。
assert.match(css, /\.modal\[data-aksrt-affiliate-poster-modal\] \.but\.cir\s*\{[^}]*border-radius:\s*5em/s)
// 36×36 与投影仍由 main.css 的 .poster-share .but.cir 提供。
const mainCss = read('assets', 'css', 'main.css')
assert.match(mainCss, /\.poster-share \.but\.cir\s*\{[^}]*width:\s*36px/s)
})
test('invite-code input verifies live against the public REST endpoint', () => {
const template = read('templates', 'parts', 'auth-content.php')
assert.match(template, /data-invite-verify/)
assert.match(template, /data-invite-feedback/)
const script = read('assets', 'js', 'auth.js')
assert.match(script, /function initInviteVerify\(root\)/)
assert.match(script, /input\[data-invite-verify\]/)
assert.match(script, /initInviteVerify\(root\)/)
assert.match(script, /\/wp-json\/aksrt\/v1\/invite-code\/verify\?code=/)
})
test('qrcode asset is enqueued only for profile + affiliate and credits depends on it', () => {
const scripts = read('src', 'Core', 'ScriptsPart1b.php')
assert.match(scripts, /assets\/js\/vendor\/qrcode\.min\.js/)
assert.match(scripts, /'aksrt-qrcode'/)
// credits.js 的依赖数组必须在 enqueue 调用内合并 QR 库句柄。
assert.match(scripts, /array_merge\(\['loader_js', 'aksrt-vip-coordinator'\], \$affiliate_deps\)/)
})
test('client IP honours the trusted-proxy filter before CDN headers', () => {
const captcha = read('inc', 'modules', 'captcha.php')
assert.match(captcha, /apply_filters\('aksrt_trusted_proxies', \[\]\)/)
assert.match(captcha, /function aksrt_ip_in_list\(string \$ip, array \$list\): bool/)
// CIDR 比较必须先检查同族地址长度,避免跨族误判。
assert.match(captcha, /strlen\(\$ip_binary\) !== strlen\(\$subnet_binary\)/)
// 过滤命中失败时直接回落 REMOTE_ADDR,不读转发头。
assert.match(captcha, /!aksrt_ip_in_list\(\$remote, \$trusted\)/)
})