141 lines
12 KiB
JavaScript
141 lines
12 KiB
JavaScript
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('月卡 <script>'));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('银联 <img>'));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('解锁 <短剧>'));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();}
|
||
});
|