const test=require('node:test'); const assert=require('node:assert/strict'); const path=require('node:path'); const createLoader=require('./load-module.cjs'); function setup(action,info) { let definition;const calls=[],toasts=[],events=[]; const api={coupleInfo:async()=>({partner1:{id:1,nickname:'本人',avatar:'uploads/me.jpg'},partner2:{id:2,nickname:'伴侣',avatar:'uploads/partner.jpg'}}),smokingAction:async payload=>{calls.push(payload);return action(payload);},smokingInfo:info||(async()=>({stock:5,today_count:0,logs:[],daily:[],available:true,read_only:false}))}; const load=createLoader({Component:c=>definition=c,Date,Math,Promise,wx:{getStorageSync:()=>1,showToast:v=>toasts.push(v),showModal:o=>o.success({confirm:true}),navigateTo(){}}},{'../../utils/request':{api,getImageUrl:value=>value?'https://past.sundynix.cn/backend/'+value:''}}); load(path.join(__dirname,'../components/smoking-card/smoking-card.js')); const component={...definition.methods,properties:{detailed:false},data:JSON.parse(JSON.stringify(definition.data)),setData(values){Object.assign(this.data,values);},triggerEvent:name=>events.push(name)}; component.applyInfo({stock:5,today_count:0,logs:[],daily:[],available:true,read_only:false}); return {component,calls,toasts,events}; } const result=()=>({success:true,info:{stock:4,today_count:1,logs:[],daily:[],available:true,read_only:false}}); test('double tap records once and uncertain network retries reuse the idempotency key',async()=>{ let resolve;const e=setup(()=>new Promise(done=>resolve=done));const first=e.component.smoke();await e.component.smoke();assert.equal(e.calls.length,1);resolve(result());await first;assert.equal(e.component.data.info.stock,4);assert.equal(e.events.length,1); let fail=true;const retry=setup(async()=>{if(fail)throw Error('offline');return result();});await retry.component.smoke();fail=false;await retry.component.smoke();assert.equal(retry.calls[0].request_key,retry.calls[1].request_key); }); test('partner view is read only and received-then-smoked is a single atomic operation',async()=>{ const e=setup(async()=>result());e.component.data.info.read_only=true;await e.component.smoke();assert.equal(e.calls.length,0); e.component.data.info.read_only=false;await e.component.receiveSmoke();assert.equal(e.calls[0].kind,'receive_smoke');assert.equal(e.calls[0].quantity,1); }); test('purchase converts packs to pieces and cents; invalid quantities never submit',async()=>{ const e=setup(async()=>result());e.component.replenish();e.component.setData({quantity:'2',packSize:'20',amount:'30.50'});await e.component.submit();assert.equal(e.calls[0].quantity,40);assert.equal(e.calls[0].amount_cents,3050); e.component.setData({quantity:'1.5'});await e.component.submit();assert.equal(e.calls.length,1); }); test('undo includes the original record and fills a seven-day chart with zero days',async()=>{ const e=setup(async()=>result());e.component.undo({currentTarget:{dataset:{id:17}}});await Promise.resolve();await Promise.resolve();assert.equal(e.calls[0].kind,'undo');assert.equal(e.calls[0].original_id,17);assert.equal(e.component.data.daily.length,7); }); test('string zero is active while one is undone, and ledger identity follows the selected account',async()=>{ const e=setup(async()=>result(),async uid=>({user_id:uid||1,owner_name:uid?'伴侣':'本人',stock:0,available:true,read_only:!!uid,logs:[{id:1,kind:'receive_smoke',undone:'0',delta:'0'},{id:2,kind:'consume',undone:'1',delta:'-1'},{id:3,kind:'consume',undone:0,delta:-1}],daily:[]})); await e.component.load(); assert.equal(e.component.data.logs[0].undone,false);assert.equal(e.component.data.logs[1].undone,true);assert.equal(e.component.data.logs[2].undone,false); assert.equal(e.component.data.ownerName,'本人');assert.ok(e.component.data.ownerAvatar.endsWith('/uploads/me.jpg')); await e.component.switchOwner({currentTarget:{dataset:{user:2}}}); assert.equal(e.component.data.ownerName,'伴侣');assert.ok(e.component.data.ownerAvatar.endsWith('/uploads/partner.jpg')); e.component.avatarFailed();assert.equal(e.component.data.ownerAvatar,'');assert.equal(e.component.data.ownerInitial,'伴'); }); test('giving a cigarette submits one stock operation, labels its ledger and rejects empty stock or partner view',async()=>{ const e=setup(async()=>({success:true,info:{user_id:1,stock:4,today_count:0,total_count:0,logs:[{id:9,kind:'give',delta:'-1',balance:'4',undone:'0'}],daily:[],available:true,read_only:false}})); await e.component.giveSmoke();assert.equal(e.calls[0].kind,'give');assert.equal(e.calls[0].quantity,1);assert.equal(e.component.data.info.today_count,0); assert.equal(e.component.data.logs[0].title,'递给别人一支');assert.equal(e.component.data.logs[0].change,'-1');assert.equal(e.component.data.logs[0].undone,false); e.component.data.info.stock=0;await e.component.giveSmoke();assert.equal(e.calls.length,1);assert.equal(e.toasts.at(-1).title,'库存不足,请先补充库存'); e.component.data.info.stock=5;e.component.data.info.read_only=true;await e.component.giveSmoke();assert.equal(e.calls.length,1); });