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
+8
View File
@@ -0,0 +1,8 @@
import {test} from 'node:test'
import assert from 'node:assert/strict'
import {cellAtPoint} from '../src/core/canvas.ts'
test('页面滚动和画布偏移后仍准确定位格子',()=>{
assert.equal(cellAtPoint(125,345,{left:100,top:300,width:100,height:100},10,10),42)
assert.equal(cellAtPoint(125,45,{left:100,top:0,width:100,height:100},10,10),42)
assert.equal(cellAtPoint(125,-1,{left:100,top:0,width:100,height:100},10,10),null)
})
+5
View File
@@ -0,0 +1,5 @@
import {test} from 'node:test'
import {strict as assert} from 'node:assert'
import {creationSize,readDraft} from '../src/core/creation.ts'
test('制作尺寸拒绝小数、空值与超限',()=>{assert.equal(creationSize('29'),29);for(let value of ['', '8.5','97','7'])assert.throws(()=>creationSize(value))})
test('草稿恢复允许空白画布并拒绝未知色号与损坏格数',()=>{let draft={title:'草稿',width:8,height:8,cells:Array(64).fill(null),colors:[]};assert.equal(readDraft(draft).cells.length,64);assert.throws(()=>readDraft({...draft,cells:['BAD',...Array(63).fill(null)]}));assert.throws(()=>readDraft({...draft,cells:[]}))})
+5
View File
@@ -0,0 +1,5 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {route,mainRoute,returnAction} from '../src/core/navigation.ts'
test('分包参数编码,空参数省略',()=>{assert.equal(route('packages/patterns/detail',{id:'a?b&c',empty:undefined}),'/packages/patterns/detail?id=a%3Fb%26c');assert.equal(route('/packages/account/library',{type:'works'}),'/packages/account/library?type=works')})
test('主包返回使用tab路由,无历史时不navigateBack',()=>{assert.equal(mainRoute('make'),'/pages/make/index');assert.deepEqual(returnAction(1,'store'),{type:'tab',url:'/pages/store/index'});assert.deepEqual(returnAction(2,'mine'),{type:'back'})})
+39
View File
@@ -0,0 +1,39 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { EndpointRouter } from '../src/core/network.ts'
let local = 'https://lan.example.com'
let remote = 'https://api.example.com'
test('蜂窝网络与未知网络不探测内网', async () => {
for (let type of ['4g', '5g', 'unknown']) {
let probes = 0
let r = new EndpointRouter({ local, remote, storeId: 's1' }, async () => { probes++; return 's1' })
assert.equal(await r.select(type), remote)
assert.equal(probes, 0)
}
})
test('Wi-Fi 只有对应门店探测成功才走内网,失败进入冷却', async () => {
let probes = 0
let r = new EndpointRouter({ local, remote, storeId: 's1' }, async () => { probes++; return 's2' })
assert.equal(await r.select('wifi'), remote)
assert.equal(await r.select('wifi'), remote)
assert.equal(probes, 1)
let good = new EndpointRouter({ local, remote, storeId: 's1' }, async () => 's1')
assert.equal(await good.select('wifi'), local)
})
test('网络切换后丢弃过期内网探测结果', async () => {
let resolve!: (value: string) => void
let r = new EndpointRouter({ local, remote, storeId: 's1' }, () => new Promise(res => { resolve = res }))
let old = r.select('wifi')
assert.equal(await r.select('4g'), remote)
resolve('s1')
assert.equal(await old, remote)
assert.equal(r.current, remote)
})
test('无网络明确离线;连通失败切回公网', async () => {
let r = new EndpointRouter({ local, remote, storeId: 's1' }, async () => 's1')
await assert.rejects(r.select('none'), /网络/)
await r.select('wifi')
r.fallback()
assert.equal(r.current, remote)
})
+23
View File
@@ -0,0 +1,23 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { quantize, countBeads, validateGrid, cropRect } from '../src/core/pattern.ts'
let colors = [{ code: 'B', name: '黑', hex: '#000000', location: '', available: true }, { code: 'W', name: '白', hex: '#ffffff', location: '', available: true }]
test('透明像素不是白色豆;统计仅包括实际豆子', () => {
let cells = quantize(new Uint8ClampedArray([255,255,255,255, 0,0,0,255, 255,255,255,0]), colors)
assert.deepEqual(cells, ['W','B',null])
assert.deepEqual(countBeads(cells), { W: 1, B: 1 })
})
test('色板量化使用最近的可用颜色', () => {
assert.deepEqual(quantize(new Uint8ClampedArray([230,230,230,255]), colors), ['W'])
assert.throws(() => quantize(new Uint8ClampedArray([0,0,0,255]), []), /色板/)
})
test('非法尺寸、未知色号、空图不能保存', () => {
assert.throws(() => validateGrid(2, 2, ['W'], colors), /网格/)
assert.throws(() => validateGrid(1, 1, ['X'], colors), /色号/)
assert.throws(() => validateGrid(1, 1, [null], colors), /空/)
assert.doesNotThrow(() => validateGrid(1, 1, ['W'], colors))
})
test('中心裁剪保持目标比例', () => {
assert.deepEqual(cropRect(400,200,1), { x:100,y:0,width:200,height:200 })
assert.deepEqual(cropRect(200,400,1), { x:0,y:100,width:200,height:200 })
})
+20
View File
@@ -0,0 +1,20 @@
import {test} from 'node:test'
import assert from 'node:assert/strict'
import {BusinessError,requestWithFallback,submitAndRefresh} from '../src/core/transport.ts'
test('内网写入结果不明时,公网重放保持相同请求标识与内容',async()=>{
let body={requestId:'one-key',kind:'help'},calls:any[]=[]
let task=await requestWithFallback('lan','lan','public','/service-requests','POST',body,async(base:string,_path:string,_method:string,data:any)=>{calls.push({base,data});if(base==='lan')throw new Error('timeout');return{id:'same-task'}},()=>{})
assert.deepEqual(task,{id:'same-task'});assert.equal(calls.length,2);assert.equal(calls[0].data,calls[1].data)
})
test('业务错误不切换、不重放;无幂等键的写入不自动重试',async()=>{
let attempts=0
await assert.rejects(requestWithFallback('lan','lan','public','/patterns','POST',{},async()=>{attempts++;throw new BusinessError('无权限',401)},()=>{}),/无权限/)
assert.equal(attempts,1)
await assert.rejects(requestWithFallback('lan','lan','public','/patterns','POST',{},async()=>{attempts++;throw new Error('timeout')},()=>{}),/确认记录/)
assert.equal(attempts,2)
})
test('提交成功但刷新失败仍保留已提交任务,不误报提交失败',async()=>{
let committed:any=null
let result=await submitAndRefresh(async()=>({id:'task-1'}),(t:any)=>{committed=t},async()=>{throw new Error('列表暂不可用')})
assert.equal(committed.id,'task-1');assert.equal(result.task.id,'task-1');assert.match(result.refreshError,/列表暂不可用/)
})
+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)
})