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>
249 lines
15 KiB
JavaScript
249 lines
15 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')
|
||
// 推广海报 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\)/)
|
||
})
|