Files
wordpress-theme-aksrt/tests/widget-system/referral-qr-contract.test.cjs
T
kate522andClaude Fable 5 ef15416ef7 fix(revenue): keep the poster modal download button circular
The blue download anchor rendered square while the adjacent yellow close
button stayed round. Both carry .but.cir, so the asymmetry had to come
from a rule matching only one of them.

credits.css:334 scopes .uc-content .but.jb-blue to a flat #3b82f6 skin
with border-radius:4px — that rule exists to stop the same class
flattening the top-nav publish chip and the site-wide login/submit/badge
buttons, so it stays. But the poster modal is emitted inside .uc-content,
and at (0,3,0) loading after main.css it out-ranks .poster-share
.but.cir's (0,2,0) on both counts, overriding the 5em pill with 4px on
the blue button only. jb-yellow is not in that selector, which is exactly
why its neighbour survived.

Restate the pill at matching weight, scoped to this modal. Padding comes
along because .uc-content .but.jb-blue also sets 0.45em 1em, which would
otherwise re-inflate the 36px circle. The 36x36 box and drop shadow stay
with .poster-share .but.cir in main.css.

Add a contract test for the radius restatement and the sizing it relies
on; verified it fails when the selector is renamed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-21 20:17:50 +08:00

192 lines
11 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')
assert.match(attribution, /'poster' => \(array\) \(\$affiliate\['poster'\] \?\? \['enabled' => false\]\)/)
assert.match(attribution, /'site_name' => get_bloginfo\('name'\)/)
const script = read('assets', 'js', 'credits.js')
// 海报仅在开关开启时暴露按钮;绘制含顶部图、文案换行、二维码与站点名。
assert.match(script, /initAffiliatePoster\(root, url \|\| j\.url \|\| '', j\.poster \|\| \{\}, j\.site_name \|\| document\.title\)/)
assert.match(script, /poster\.enabled !== true\) return/)
assert.match(script, /function buildPosterCanvas/)
assert.match(script, /function wrapText\(ctx, text, maxWidth\)/)
assert.match(script, /image\.crossOrigin = 'anonymous'/)
})
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\)/)
})