BluetoothPrinter.test.cjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. const {test} = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const vm = require('node:vm');
  5. const path = require('node:path');
  6. function load(write) {
  7. const source = fs.readFileSync(path.join(__dirname, 'BluetoothPrinter.js'), 'utf8')
  8. .replace(/export function /g, 'function ')
  9. .replace('export default BluetoothPrinter;', 'globalThis.Printer = BluetoothPrinter; globalThis.encode = typeof encodeMonoRaster === "function" ? encodeMonoRaster : undefined;');
  10. const context = {ArrayBuffer, Uint8Array, Date, Promise, setTimeout, clearTimeout,
  11. console: {log(){}, warn(){}, error(){}},
  12. uni: {getSystemInfoSync: () => ({platform:'android'}), writeBLECharacteristicValue: write}};
  13. vm.runInNewContext(source, context);
  14. return context;
  15. }
  16. function printer(write) {
  17. const context = load(write);
  18. const p = new context.Printer({writeTimeoutMs:30});
  19. Object.assign(p, {isConnected:true, deviceId:'device', serviceId:'service', characteristicId:'characteristic'});
  20. return p;
  21. }
  22. // 独立解码GS v 0;将白色窄光栅补回完整宽度,验证高度和全部黑色像素不变。
  23. function decode(buffer, width) {
  24. const bytes = new Uint8Array(buffer), rows = [];
  25. for (let i=0; i<bytes.length;) {
  26. assert.deepEqual(Array.from(bytes.slice(i,i+4)), [29,118,48,0]);
  27. const stride=bytes[i+4]+bytes[i+5]*256, height=bytes[i+6]+bytes[i+7]*256;
  28. assert.ok(stride>0 && stride<=width/8 && height>0);
  29. i+=8;
  30. assert.ok(i+stride*height<=bytes.length);
  31. for(let y=0;y<height;y++) {
  32. const row = new Uint8Array(width/8);
  33. row.set(bytes.slice(i,i+stride)); rows.push(...row); i+=stride;
  34. }
  35. }
  36. return new Uint8Array(rows);
  37. }
  38. test('混合空白与非空行压缩后逐像素和高度一致', () => {
  39. const {encode} = load();
  40. const data = new Uint8Array(48*720);
  41. for(let y=110;y<320;y++) for(let x=0;x<48;x++) data[y*48+x]=(x*37+y)%256;
  42. const encoded = encode(data,384,720);
  43. assert.deepEqual(decode(encoded,384),data);
  44. assert.ok(encoded.byteLength < data.byteLength*0.5);
  45. });
  46. test('黑白交替、全白、全黑、短空白和2048行边界不改变数据', () => {
  47. const {encode} = load();
  48. for (const height of [1,7,8,255,2048,2050]) for(const mode of ['white','black','alternating']) {
  49. const data=new Uint8Array(48*height);
  50. for(let y=0;y<height;y++) if(mode==='black'||mode==='alternating'&&y%2) data.fill(255,y*48,(y+1)*48);
  51. const encoded=encode(data,384,height);
  52. assert.deepEqual(decode(encoded,384),data);
  53. assert.ok(encoded.byteLength<=data.byteLength+8*Math.ceil(height/2048));
  54. }
  55. });
  56. test('拒绝PNG或长度/尺寸不正确的点阵,关闭优化仍可正常还原', () => {
  57. const {encode} = load();
  58. assert.throws(()=>encode(new Uint8Array(7),384,2));
  59. assert.throws(()=>encode(new Uint8Array(48),383,1));
  60. assert.throws(()=>encode(new Uint8Array(0),384,0));
  61. const data=new Uint8Array(48*100);
  62. const result=encode(data,384,100,{compactBlankRows:false});
  63. assert.equal(result.byteLength,4808);
  64. assert.deepEqual(decode(result,384),data);
  65. });
  66. test('默认20字节且原始数据顺序不变',async()=>{
  67. const chunks=[];
  68. const p=printer(o=>{chunks.push(new Uint8Array(o.value));o.success({errMsg:'ok'});});
  69. const data=Uint8Array.from({length:43},(_,i)=>i);
  70. await p.sendPrintData(data.buffer);
  71. assert.deepEqual(chunks.map(c=>c.length),[20,20,3]);
  72. assert.deepEqual(Uint8Array.from(chunks.flatMap(c=>Array.from(c))),data);
  73. });
  74. test('已验证的连接传输参数降低分包数,且限制作用于当前设备',async()=>{
  75. const chunks=[];
  76. const p=printer(o=>{chunks.push(o.value.byteLength);o.success({errMsg:'ok'});});
  77. assert.throws(()=>p.setTransportProfile({deviceId:'other',maxWriteBytes:180,writeDelayMs:0}));
  78. assert.throws(()=>p.setTransportProfile({deviceId:'device',maxWriteBytes:0}));
  79. p.setTransportProfile({deviceId:'device',maxWriteBytes:180,writeDelayMs:0});
  80. await p.sendPrintData(new ArrayBuffer(361));
  81. assert.deepEqual(chunks,[180,180,1]);
  82. });
  83. test('整次发送互斥,进度节流且最终进度必达',async()=>{
  84. let first;
  85. const p=printer(o=>{if(!first)first=o;else o.success({errMsg:'ok'});});
  86. p.setTransportProfile({deviceId:'device',maxWriteBytes:20,writeDelayMs:0});
  87. const progress=[];
  88. const pending=p.sendPrintData(new ArrayBuffer(2000),(a,b)=>progress.push([a,b]));
  89. await assert.rejects(p.sendPrintData(new ArrayBuffer(20)),/进行中/);
  90. first.success({errMsg:'ok'});
  91. await pending;
  92. assert.ok(progress.length<10);
  93. assert.deepEqual(progress.at(-1),[2000,2000]);
  94. });
  95. test('写入失败不重试,禁止后续任务接在可能残缺的光栅后',async()=>{
  96. let writes=0;
  97. const p=printer(o=>{writes++;o.fail({errCode:10008,errMsg:'fail'});});
  98. await assert.rejects(p.sendPrintData(new ArrayBuffer(40)));
  99. await assert.rejects(p.sendPrintData(new ArrayBuffer(40)),/中断/);
  100. assert.equal(writes,1);
  101. });
  102. test('写入无回调会超时,不重发数据',async()=>{
  103. let writes=0;
  104. const p=printer(()=>writes++);
  105. await assert.rejects(p.sendPrintData(new ArrayBuffer(40)),/超时/);
  106. assert.equal(writes,1);
  107. });
  108. test('UI进度回调异常不重发或中断数据',async()=>{
  109. let bytes=0;
  110. const p=printer(o=>{bytes+=o.value.byteLength;o.success({errMsg:'ok'});});
  111. await p.sendPrintData(new ArrayBuffer(21),()=>{throw new Error('UI failure');});
  112. assert.equal(bytes,21);
  113. });
  114. module.exports={load,decode};
  115. test('错误后的人工恢复与传输参数清理',async()=>{
  116. let fail=true;
  117. const p=printer(o=>fail?o.fail({errCode:10007}):o.success({errMsg:'ok'}));
  118. p.setTransportProfile({deviceId:'device',maxWriteBytes:180,writeDelayMs:0});
  119. await assert.rejects(p.sendPrintData(new ArrayBuffer(8)));
  120. fail=false;
  121. p.confirmPrinterBufferCleared();
  122. await p.sendPrintData(new ArrayBuffer(8));
  123. // 断开时禁止把已测连接的包长沿用到另一台打印机。
  124. p.isConnected=false;
  125. p.disconnect();
  126. assert.equal(p.transportProfile,null);
  127. });
  128. test('超时后的迟到成功回调不会继续发送余下数据',async()=>{
  129. let late, writes=0;
  130. const p=printer(o=>{late=o;writes++;});
  131. await assert.rejects(p.sendPrintData(new ArrayBuffer(41)),/超时/);
  132. late.success({errMsg:'ok'});
  133. await Promise.resolve();
  134. assert.equal(writes,1);
  135. await assert.rejects(p.sendPrintData(new ArrayBuffer(1)),/中断/);
  136. });
  137. // 可选地传入由真实PNG解码得到的点阵样张;没有样张时仍运行全部独立单元测试。
  138. if (process.env.INVOICE_RASTER_SAMPLES) {
  139. const samples=JSON.parse(fs.readFileSync(process.env.INVOICE_RASTER_SAMPLES,'utf8'));
  140. for(const sample of samples) test(`实际发票样张像素还原:${sample.name}`,()=>{
  141. const data=new Uint8Array(Buffer.from(sample.raster,'base64'));
  142. const {encode}=load();
  143. const before=encode(data,sample.width,sample.height,{compactBlankRows:false});
  144. const after=encode(data,sample.width,sample.height);
  145. assert.deepEqual(decode(after,sample.width),data);
  146. assert.ok(after.byteLength<=before.byteLength);
  147. console.log(JSON.stringify({name:sample.name,width:sample.width,height:sample.height,
  148. pngBytes:sample.pngBytes,before:before.byteLength,after:after.byteLength,
  149. savedPercent:Math.round((1-after.byteLength/before.byteLength)*1000)/10}));
  150. });
  151. }