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>
192 lines
11 KiB
JavaScript
192 lines
11 KiB
JavaScript
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\)/)
|
||
})
|