const {test} = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const path = require('node:path'); function load(write) { const source = fs.readFileSync(path.join(__dirname, 'BluetoothPrinter.js'), 'utf8') .replace(/export function /g, 'function ') .replace('export default BluetoothPrinter;', 'globalThis.Printer = BluetoothPrinter; globalThis.encode = typeof encodeMonoRaster === "function" ? encodeMonoRaster : undefined;'); const context = {ArrayBuffer, Uint8Array, Date, Promise, setTimeout, clearTimeout, console: {log(){}, warn(){}, error(){}}, uni: {getSystemInfoSync: () => ({platform:'android'}), writeBLECharacteristicValue: write}}; vm.runInNewContext(source, context); return context; } function printer(write) { const context = load(write); const p = new context.Printer({writeTimeoutMs:30}); Object.assign(p, {isConnected:true, deviceId:'device', serviceId:'service', characteristicId:'characteristic'}); return p; } // 独立解码GS v 0;将白色窄光栅补回完整宽度,验证高度和全部黑色像素不变。 function decode(buffer, width) { const bytes = new Uint8Array(buffer), rows = []; for (let i=0; i0 && stride<=width/8 && height>0); i+=8; assert.ok(i+stride*height<=bytes.length); for(let y=0;y { const {encode} = load(); const data = new Uint8Array(48*720); for(let y=110;y<320;y++) for(let x=0;x<48;x++) data[y*48+x]=(x*37+y)%256; const encoded = encode(data,384,720); assert.deepEqual(decode(encoded,384),data); assert.ok(encoded.byteLength < data.byteLength*0.5); }); test('黑白交替、全白、全黑、短空白和2048行边界不改变数据', () => { const {encode} = load(); for (const height of [1,7,8,255,2048,2050]) for(const mode of ['white','black','alternating']) { const data=new Uint8Array(48*height); for(let y=0;y { const {encode} = load(); assert.throws(()=>encode(new Uint8Array(7),384,2)); assert.throws(()=>encode(new Uint8Array(48),383,1)); assert.throws(()=>encode(new Uint8Array(0),384,0)); const data=new Uint8Array(48*100); const result=encode(data,384,100,{compactBlankRows:false}); assert.equal(result.byteLength,4808); assert.deepEqual(decode(result,384),data); }); test('默认20字节且原始数据顺序不变',async()=>{ const chunks=[]; const p=printer(o=>{chunks.push(new Uint8Array(o.value));o.success({errMsg:'ok'});}); const data=Uint8Array.from({length:43},(_,i)=>i); await p.sendPrintData(data.buffer); assert.deepEqual(chunks.map(c=>c.length),[20,20,3]); assert.deepEqual(Uint8Array.from(chunks.flatMap(c=>Array.from(c))),data); }); test('已验证的连接传输参数降低分包数,且限制作用于当前设备',async()=>{ const chunks=[]; const p=printer(o=>{chunks.push(o.value.byteLength);o.success({errMsg:'ok'});}); assert.throws(()=>p.setTransportProfile({deviceId:'other',maxWriteBytes:180,writeDelayMs:0})); assert.throws(()=>p.setTransportProfile({deviceId:'device',maxWriteBytes:0})); p.setTransportProfile({deviceId:'device',maxWriteBytes:180,writeDelayMs:0}); await p.sendPrintData(new ArrayBuffer(361)); assert.deepEqual(chunks,[180,180,1]); }); test('整次发送互斥,进度节流且最终进度必达',async()=>{ let first; const p=printer(o=>{if(!first)first=o;else o.success({errMsg:'ok'});}); p.setTransportProfile({deviceId:'device',maxWriteBytes:20,writeDelayMs:0}); const progress=[]; const pending=p.sendPrintData(new ArrayBuffer(2000),(a,b)=>progress.push([a,b])); await assert.rejects(p.sendPrintData(new ArrayBuffer(20)),/进行中/); first.success({errMsg:'ok'}); await pending; assert.ok(progress.length<10); assert.deepEqual(progress.at(-1),[2000,2000]); }); test('写入失败不重试,禁止后续任务接在可能残缺的光栅后',async()=>{ let writes=0; const p=printer(o=>{writes++;o.fail({errCode:10008,errMsg:'fail'});}); await assert.rejects(p.sendPrintData(new ArrayBuffer(40))); await assert.rejects(p.sendPrintData(new ArrayBuffer(40)),/中断/); assert.equal(writes,1); }); test('写入无回调会超时,不重发数据',async()=>{ let writes=0; const p=printer(()=>writes++); await assert.rejects(p.sendPrintData(new ArrayBuffer(40)),/超时/); assert.equal(writes,1); }); test('UI进度回调异常不重发或中断数据',async()=>{ let bytes=0; const p=printer(o=>{bytes+=o.value.byteLength;o.success({errMsg:'ok'});}); await p.sendPrintData(new ArrayBuffer(21),()=>{throw new Error('UI failure');}); assert.equal(bytes,21); }); module.exports={load,decode}; test('错误后的人工恢复与传输参数清理',async()=>{ let fail=true; const p=printer(o=>fail?o.fail({errCode:10007}):o.success({errMsg:'ok'})); p.setTransportProfile({deviceId:'device',maxWriteBytes:180,writeDelayMs:0}); await assert.rejects(p.sendPrintData(new ArrayBuffer(8))); fail=false; p.confirmPrinterBufferCleared(); await p.sendPrintData(new ArrayBuffer(8)); // 断开时禁止把已测连接的包长沿用到另一台打印机。 p.isConnected=false; p.disconnect(); assert.equal(p.transportProfile,null); }); test('超时后的迟到成功回调不会继续发送余下数据',async()=>{ let late, writes=0; const p=printer(o=>{late=o;writes++;}); await assert.rejects(p.sendPrintData(new ArrayBuffer(41)),/超时/); late.success({errMsg:'ok'}); await Promise.resolve(); assert.equal(writes,1); await assert.rejects(p.sendPrintData(new ArrayBuffer(1)),/中断/); }); // 可选地传入由真实PNG解码得到的点阵样张;没有样张时仍运行全部独立单元测试。 if (process.env.INVOICE_RASTER_SAMPLES) { const samples=JSON.parse(fs.readFileSync(process.env.INVOICE_RASTER_SAMPLES,'utf8')); for(const sample of samples) test(`实际发票样张像素还原:${sample.name}`,()=>{ const data=new Uint8Array(Buffer.from(sample.raster,'base64')); const {encode}=load(); const before=encode(data,sample.width,sample.height,{compactBlankRows:false}); const after=encode(data,sample.width,sample.height); assert.deepEqual(decode(after,sample.width),data); assert.ok(after.byteLength<=before.byteLength); console.log(JSON.stringify({name:sample.name,width:sample.width,height:sample.height, pngBytes:sample.pngBytes,before:before.byteLength,after:after.byteLength, savedPercent:Math.round((1-after.byteLength/before.byteLength)*1000)/10})); }); }