Files
web_hgdjc_h5/tests/payment.test.mjs
T
谢宇宁andClaude Opus 5.5 f65d10e998 Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:07:59 +07:00

141 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.
import {routingModuleUrl} from './helpers/routing.mjs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
async function moduleUrl(file,replace=code=>code){
const filename=new URL(file,import.meta.url);
const {outputText}=ts.transpileModule(await readFile(filename,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}});
return 'data:text/javascript;base64,'+Buffer.from(replace(outputText).replace(/(['"])\.\.\/routing\1/g,JSON.stringify(routingModuleUrl))+'\n//# sourceURL='+filename.pathname).toString('base64');
}
const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts');
const renderUrl=await moduleUrl('../src/lib/client/render.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)));
const authUrl=await moduleUrl('../src/lib/client/auth.ts');
const trackingUrl=await moduleUrl('../src/lib/tracking.ts');
const accountUrl=await moduleUrl('../src/lib/client/account.ts',code=>code.replace('"../api/protocol"',JSON.stringify(protocolUrl)).replace('"./render"',JSON.stringify(renderUrl)).replace("'./auth'",JSON.stringify(authUrl)).replace("'../tracking'",JSON.stringify(trackingUrl)));
const {paymentDialogMarkup,renderWallet}=await import(accountUrl);
const coinPackage={id:31,name:'金币套餐',packageType:2,price:10000,baseCoin:100,giftCoin:999,totalCoin:1099,payMethods:[{type:'alipay',typeName:'支付宝'},{type:'wechat',typeName:'微信'}]};
const flush=()=>new Promise(resolve=>setImmediate(resolve));
test('confirmation displays actual base/gift/total coins and the price in yuan with original payment icons',()=>{
const html=paymentDialogMarkup(coinPackage,false);
assert.ok(html.includes('<strong>100 金币</strong>'));assert.ok(html.includes('赠送 999 金币 · 合计 1099 金币'));
assert.ok(html.includes('<b>¥100</b>'));assert.ok(html.includes('5652fcead17188e8.svg'));assert.ok(html.includes('2bead9ea5c435c98.svg'));
assert.ok(!html.includes('7848816d05aa8591.svg'));assert.ok(!html.includes('type="radio"'));
assert.ok(html.includes('name="payMode" value="alipay"'));assert.ok(html.includes('class="wallet-payment-title"'));
const derived=paymentDialogMarkup({...coinPackage,baseCoin:undefined,price:12345},false);
assert.ok(derived.includes('<strong>100 金币</strong>'));assert.ok(derived.includes('<b>¥123.45</b>'));
});
test('VIP and missing payment data never introduce demo bonuses or unconfigured channels',()=>{
const vip=paymentDialogMarkup({id:7,name:'月卡 <script>',packageType:1,price:3000},true);
assert.ok(vip.includes('会员畅享'));assert.ok(vip.includes('月卡 &lt;script&gt;'));assert.ok(!vip.includes('赠送 999'));
assert.ok(vip.includes('暂无可用支付方式'));assert.match(vip,/<button class="wallet-submit wallet-payment-go" type="submit" disabled>/);
assert.ok(!vip.includes('data-wallet-method='));
const unknown=paymentDialogMarkup({...coinPackage,payMethods:[null,{type:'',typeName:'空'},{type:'union',typeName:'银联 <img>'},{type:'union',typeName:'重复'}]},false);
assert.equal((unknown.match(/data-wallet-method=/g)||[]).length,1);assert.ok(unknown.includes('银联 &lt;img&gt;'));assert.ok(unknown.includes('wallet-pay-logo--other'));
});
class Element extends EventTarget {
dataset={};attrs=new Map();nodes=new Map();lists=new Map();innerHTML='';textContent='';disabled=false;isConnected=true;value='';
querySelector(selector){return this.nodes.get(selector)||null;}
querySelectorAll(selector){return this.lists.get(selector)||[];}
setAttribute(key,value){this.attrs.set(key,String(value));}
getAttribute(key){return this.attrs.get(key)||null;}
closest(){return this;}
reportValidity(){return true;}
}
function emit(element,type,target=element){const event=new Event(type,{cancelable:true});Object.defineProperty(event,'target',{value:target});element.dispatchEvent(event);}
async function fixture({vip=false,post=async()=>({payUrl:'https://pay.example/checkout',tradeNo:'order-1'}),methods=coinPackage.payMethods,packages,transactions={list:[],totalCount:0},transactionError=false,balance=90,openDialog=true}={}){
const oldDocument=globalThis.document,OldFormData=globalThis.FormData;
const controller=new AbortController(),root=new Element(),dialog=new Element(),form=new Element(),hidden=new Element(),confirm=new Element(),error=new Element(),refresh=new Element();
const item={...coinPackage,payMethods:methods};hidden.value=methods[0]?.type||'';
const items=packages??[item],pay=new Element(),selection=new Element();pay.dataset.walletAction='pay';
const packageButtons=items.map((item,index)=>{const button=new Element();button.dataset.buyPackage=String(item.id);button.setAttribute('aria-pressed',String(index===0));return button;});
root.nodes.set('[data-wallet-action="pay"]',pay);root.nodes.set('#wallet-selection',selection);root.lists.set('[data-buy-package]',packageButtons);
const choices=methods.map(method=>{const button=new Element();button.dataset.walletMethod=method.type;button.nodes.set('.wallet-method-check',new Element());return button;});
form.nodes.set('[name="payMode"]',hidden);form.nodes.set('[type="submit"]',confirm);form.nodes.set('.api-error',error);form.nodes.set('[data-payment-refresh]',refresh);form.lists.set('[data-wallet-method]',choices);
dialog.nodes.set('form',form);dialog.open=false;dialog.showModal=()=>dialog.open=true;dialog.close=()=>dialog.open=false;
const posts=[];let refreshes=0,routes=0;
const user={recharge:balance};
const app={signal:controller.signal,api:{user,ensureSession:async()=>user,get:async path=>{if(path==='recharge/package/list')return {coinList:items,vipList:items};if(transactionError)throw new Error('offline');return transactions;},post:async(path,body)=>{posts.push({path,body});return post();},refreshUser:async()=>refreshes++},notify(){},async route(){routes++;}};
globalThis.document={getElementById:id=>id==='info-content'?root:dialog};
globalThis.FormData=class extends OldFormData{constructor(form){super();this.append('payMode',form.querySelector('[name="payMode"]').value);}};
await renderWallet(app,vip,1,()=>'/info/deposit/');
if(openDialog)emit(root,'click',pay);
return {app,root,dialog,form,hidden,confirm,error,refresh,choices,posts,pay,selection,packageButtons,get refreshes(){return refreshes;},get routes(){return routes;},dispose(){controller.abort();globalThis.document=oldDocument;globalThis.FormData=OldFormData;}};
}
test('recharge page uses the HTML balance, package and transaction layout with actual API values',async()=>{
const f=await fixture({openDialog:false,balance:2400,packages:[{...coinPackage,baseCoin:250,giftCoin:25,totalCoin:275,price:12345}],transactions:{list:[
{title:'解锁 <短剧>',createdAt:'2026-10-20T13:24:00+08:00',flowDir:-1,amount:30,changeAmount:0},
{tranTypeName:'充值到账',createdAt:'2026-10-20 12:00:00',flowDir:1,amount:100,changeAmount:125},
{remark:'余额校验',flowDir:1,amount:0,coin:999},
],totalCount:3}});
try{
const html=f.root.innerHTML;
for(const className of ['wallet-balance-card','wallet-balance-summary','wallet-usage','wallet-pack-panel','wallet-pack-heading','wallet-package-coin','wallet-double-badge','wallet-mobile-selected','wallet-pay-area','wallet-transaction-list'])assert.ok(html.includes(className),className);
assert.ok(html.includes('<strong>2,400</strong><b>金币</b>'));assert.ok(html.includes('>250金币</span>'));assert.ok(html.includes('>赠送 25金币</span>'));assert.ok(html.includes('立即支付 ¥123.45'));
assert.ok(html.includes('已选择 250金币 + 赠送25金币'));assert.ok(html.includes('aria-pressed="true"'));
assert.ok(html.includes('解锁 &lt;短剧&gt;'));assert.ok(html.includes('2026/10/20 13:24'));assert.ok(html.includes('datetime="2026-10-20T05:24:00.000Z"'));
assert.ok(html.includes('expense">−30</span>'));assert.ok(html.includes('income">+125</span>'));assert.ok(html.includes('income">0</span>'));
assert.ok(html.includes('href="/info/feedback/"'));assert.ok(!html.includes('模拟'));assert.ok(!html.includes('首次充值赠送10%'));assert.ok(!html.includes('1,280'));
assert.equal(f.dialog.open,false);assert.equal(f.posts.length,0);
}finally{f.dispose();}
});
test('package click only updates selection and the pay action confirms the selected package ID',async()=>{
const second={...coinPackage,id:84,baseCoin:300,giftCoin:0,totalCoin:300,price:4599};
const f=await fixture({openDialog:false,packages:[coinPackage,second]});
try{
emit(f.root,'click',f.packageButtons[1]);
assert.equal(f.dialog.open,false);assert.equal(f.posts.length,0);
assert.equal(f.packageButtons[0].getAttribute('aria-pressed'),'false');assert.equal(f.packageButtons[1].getAttribute('aria-pressed'),'true');
assert.equal(f.pay.textContent,'立即支付 ¥45.99');assert.equal(f.selection.textContent,'已选择 300金币 · 点击后选择支付方式');
emit(f.root,'click',f.pay);assert.equal(f.dialog.open,true);assert.ok(f.dialog.innerHTML.includes('<strong>300 金币</strong>'));assert.ok(f.dialog.innerHTML.includes('<b>¥45.99</b>'));
emit(f.form,'submit');await flush();assert.deepEqual(f.posts,[{path:'coin/buy',body:{packageId:84,payMode:'alipay',sourcePageKey:'deposit',sourcePageName:'充值中心'}}]);
}finally{f.dispose();}
});
test('empty packages and unavailable transactions show empty states without sample data or an order',async()=>{
const f=await fixture({openDialog:false,packages:[],transactionError:true,balance:0});
try{
assert.ok(f.root.innerHTML.includes('<strong>0</strong><b>金币</b>'));assert.ok(f.root.innerHTML.includes('暂无可购买套餐'));assert.ok(f.root.innerHTML.includes('余额明细暂不可用,请稍后刷新'));
assert.match(f.root.innerHTML, /data-wallet-action="pay" disabled/);assert.ok(!f.root.innerHTML.includes('data-buy-package='));assert.ok(!f.root.innerHTML.includes('wallet-transaction-row'));
emit(f.root,'click',f.pay);assert.equal(f.dialog.open,false);assert.equal(f.posts.length,0);
}finally{f.dispose();}
});
test('selecting a styled method updates the actual order payload and duplicate submission is blocked',async()=>{
let finish;const f=await fixture({post:()=>new Promise(resolve=>finish=resolve)});
try{
assert.equal(f.dialog.open,true);assert.equal(f.posts.length,0);
emit(f.form,'click',f.choices[1]);assert.equal(f.hidden.value,'wechat');assert.equal(f.choices[1].getAttribute('aria-pressed'),'true');assert.equal(f.choices[1].querySelector('.wallet-method-check').textContent,'✓');
emit(f.form,'submit');emit(f.form,'submit');assert.deepEqual(f.posts,[{path:'coin/buy',body:{packageId:31,payMode:'wechat',sourcePageKey:'deposit',sourcePageName:'充值中心'}}]);
assert.equal(f.confirm.disabled,true);emit(f.form,'click',f.choices[0]);assert.equal(f.hidden.value,'wechat');
finish({payUrl:'https://pay.example/checkout',tradeNo:'order-1'});await flush();
assert.ok(f.form.innerHTML.includes('打开支付页面'));assert.equal(f.refreshes,0);assert.equal(f.app.api.user.recharge,90);
emit(f.refresh,'click');await flush();assert.equal(f.refreshes,1);assert.equal(f.dialog.open,false);assert.equal(f.routes,1);
}finally{f.dispose();}
});
test('VIP uses its existing endpoint and an invalid payment URL leaves the form available to retry',async()=>{
const f=await fixture({vip:true,post:async()=>({payUrl:'javascript:alert(1)'})});
try{
emit(f.form,'submit');await flush();assert.deepEqual(f.posts,[{path:'vip/pay',body:{packageId:31,payMode:'alipay',sourcePageKey:'deposit',sourcePageName:'充值中心'}}]);
assert.match(f.error.textContent,/有效地址/);assert.equal(f.confirm.disabled,false);assert.equal(f.choices[0].disabled,false);assert.equal(f.refreshes,0);
assert.ok(!f.form.innerHTML.includes('javascript:'));
}finally{f.dispose();}
});
test('missing methods cannot create an order and a replaced form ignores late order responses',async()=>{
const empty=await fixture({methods:[]});
try{emit(empty.form,'submit');assert.equal(empty.posts.length,0);}finally{empty.dispose();}
let finish;const late=await fixture({post:()=>new Promise(resolve=>finish=resolve)});
try{
emit(late.form,'submit');late.form.isConnected=false;finish({payUrl:'https://pay.example',tradeNo:'old-order'});await flush();
assert.equal(late.form.innerHTML,'');assert.equal(late.refreshes,0);
}finally{late.dispose();}
});