chore(小程序): 初始化工程仓库

This commit is contained in:
leon_mac
2026-10-18 15:11:44 +08:00
commit e1975a1abf
49 changed files with 10515 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import {test} from 'node:test'
import assert from 'node:assert/strict'
import {build} from 'esbuild'
import {fileURLToPath} from 'node:url'
// api.ts 依赖 Vite 环境;只隔离环境适配层,所有工作流逻辑使用实际源代码与 Vue refs。
let bundle = await build({
stdin:{contents:"import * as workflow from './src/stores/visit.ts'; export {workflow}; export {reactive} from 'vue'",resolveDir:fileURLToPath(new URL('..',import.meta.url))},
bundle:true,platform:'node',format:'esm',write:false,
plugins:[{name:'isolate-vite-api',setup(builder){
builder.onResolve({filter:/services\/api$/},()=>({path:'vite-api',namespace:'test'}))
builder.onLoad({filter:/.*/,namespace:'test'},()=>({contents:"export const storeId='test-store';export function api(){throw new Error('Inject test API')}",loader:'js'}))
}}]
})
let module = await import('data:text/javascript;base64,'+Buffer.from(bundle.outputFiles[0].text).toString('base64'))
function fixture(){
assert.equal(typeof module.workflow.createVisitWorkflow,'function','visit workflow must expose isolated API dependencies')
let identity='user-a', failPost=false, failRefresh=false, posted:any[]=[], current:any=null
let send=async(path:string,method='GET',body?:any)=>{
if(path==='/visits/current'&&method==='DELETE'){current=null;return null}
if(path==='/visits/current')return current
if(path==='/visits'&&method==='POST')return current={id:'visit-'+body.tableCode,storeId:'test-store',tableCode:body.tableCode,status:'active'}
if(path==='/service-requests'&&method==='POST'){posted.push({...body});if(failPost)throw new Error('ambiguous network');return {id:'task-'+posted.length,userId:identity,tableCode:current.tableCode,storeId:'test-store',status:'pending',...body}}
if(failRefresh)throw new Error('refresh offline')
return []
}
let state=module.reactive(module.workflow.createVisitWorkflow({storeId:'test-store',identity:()=>identity,ensure:async()=>{},api:(...args:any[])=>send(args[0],args[1],args[2])}))
return {state,posted,getIdentity:()=>identity,setIdentity:(v:string)=>{identity=v},setCurrent:(v:any)=>{current=v},setFailPost:(v:boolean)=>{failPost=v},setFailRefresh:(v:boolean)=>{failRefresh=v},setAPI:(fn:typeof send)=>{send=fn}}
}
test('模糊失败重试复用UUID;刷新失败后保留成功且禁止再次提交',{timeout:3000},async()=>{
let f=fixture(),s=f.state;await s.confirmVisit('T01');s.note='same';f.setFailPost(true)
await assert.rejects(s.submit(),/ambiguous/);f.setFailPost(false);f.setFailRefresh(true)
let result=await s.submit();assert.deepEqual(f.posted[0],f.posted[1]);assert.match(f.posted[0].requestId,/^[0-9a-f-]{36}$/)
assert.match(result.refreshError,/offline/);assert.ok(s.submitted);await s.submit();assert.equal(f.posted.length,2)
s.startNewRequest();await s.submit();assert.notEqual(f.posted[1].requestId,f.posted[2].requestId)
})
test('首入申请去确认到店保留草稿;新到店会话清除旧结果与幂等上下文',{timeout:3000},async()=>{
let f=fixture(),s=f.state;s.configureRequest({kind:'print',patternId:'pattern-1'});s.note='keep draft'
await s.confirmVisit('T01');assert.equal(s.kind,'print');assert.equal(s.note,'keep draft');assert.equal(s.patternId,'pattern-1')
await s.submit();let id=f.posted[0].requestId;await s.leave();assert.equal(s.submitted,null)
await s.confirmVisit('T02');assert.equal(s.submitted,null);await s.submit();assert.equal(f.posted.length,2);assert.equal(s.submitted.tableCode,'T02');assert.notEqual(f.posted[1].requestId,id)
})
test('服务端刷新发现新的visit,也清除旧成功结果',{timeout:3000},async()=>{
let f=fixture(),s=f.state;await s.confirmVisit('T01');await s.submit()
f.setCurrent({id:'new-visit',storeId:'test-store',tableCode:'T02',status:'active'});await s.refreshVisit();assert.equal(s.submitted,null);assert.equal(s.visit.tableCode,'T02')
})
test('换账号清除旧结果/草稿/记录,旧账号在途读取不能回写',{timeout:3000},async()=>{
let f=fixture(),s=f.state;await s.confirmVisit('T01');s.note='private';await s.submit()
let resolve:(v:any)=>void=()=>{};f.setAPI(async()=>new Promise(r=>{resolve=r}))
let read=s.refreshTasks();f.setIdentity('user-b');s.configureRequest({})
assert.equal(s.submitted,null);assert.equal(s.visit,null);assert.equal(s.note,'');assert.equal(s.tasks.length,0)
resolve([{id:'old-user-task'}]);await read;assert.equal(s.tasks.length,0)
})
test('在途提交期间换账号不写回旧成功结果',{timeout:3000},async()=>{
let f=fixture(),s=f.state;await s.confirmVisit('T01')
let resolve:(v:any)=>void=()=>{};f.setAPI(async()=>new Promise(r=>{resolve=r}))
let posting=s.submit();f.setIdentity('user-b');s.configureRequest({})
resolve({id:'old-task',tableCode:'T01'});await assert.rejects(posting,/账号|会话/);assert.equal(s.submitted,null);assert.equal(s.tasks.length,0)
})
test('首次登录确认桌位保留草稿;登出清除成功与旧记录',{timeout:3000},async()=>{
let f=fixture(),s=f.state;f.setIdentity('');s.configureRequest({kind:'ironing'});s.note='first draft'
// initial logged-out state is the normal request → visit journey.
let initial=module.reactive(module.workflow.createVisitWorkflow({storeId:'test-store',identity:f.getIdentity,ensure:async()=>f.setIdentity('user-a'),api:async(path:string,_method:string,body:any)=>({id:'first',storeId:'test-store',tableCode:body.tableCode,status:'active'})}))
initial.configureRequest({kind:'ironing'});initial.note='first draft';await initial.confirmVisit('T01');assert.equal(initial.kind,'ironing');assert.equal(initial.note,'first draft')
f.setIdentity('user-a');await s.confirmVisit('T01');await s.submit();f.setIdentity('');await s.refreshVisit();assert.equal(s.submitted,null);assert.equal(s.visit,null);assert.equal(s.tasks.length,0)
})
test('换账号后旧到店读取不回写;未关联打印不发请求',{timeout:3000},async()=>{
let f=fixture(),s=f.state;await s.confirmVisit('T01');s.configureRequest({kind:'print'})
await assert.rejects(s.submit(),/图纸/);assert.equal(f.posted.length,0)
let resolve:(v:any)=>void=()=>{};f.setAPI(async()=>new Promise(r=>{resolve=r}))
let read=s.refreshVisit();f.setIdentity('user-b');s.configureRequest({})
resolve({id:'old-visit',storeId:'test-store',tableCode:'T01',status:'active'});await read
assert.equal(s.visit,null);assert.equal(s.submitted,null)
})