package escpos import ( "bytes" "image/png" "os" "path/filepath" "sync" "testing" ) type recorder struct { mu sync.Mutex buf []byte } func (r *recorder) reply(b []byte) { r.mu.Lock() r.buf = append(r.buf, b...) r.mu.Unlock() } func (r *recorder) take() []byte { r.mu.Lock() defer r.mu.Unlock() b := r.buf r.buf = nil return b } func newTestPrinter(t *testing.T, key string) *Printer { t.Helper() prof, ok := FindProfile(key) if !ok { t.Fatalf("perfil %q no existe", key) } p, err := NewPrinter(prof) if err != nil { t.Fatal(err) } return p } func blackPixels(p *Printer) int { n := 0 for _, t := range p.Tickets() { for _, v := range t.Image.Pix { if v < 0x80 { n++ } } } return n } func TestTextAndCut(t *testing.T) { p := newTestPrinter(t, "80mm") s := p.NewSession("test", nil) s.Write([]byte("\x1b@Hola mundo\n\x1dVA\x00")) if got := len(p.Tickets()); got != 1 { t.Fatalf("tickets = %d, want 1", got) } if blackPixels(p) == 0 { t.Fatal("el ticket está en blanco") } } func TestCutWithoutFeedLeavesTail(t *testing.T) { p := newTestPrinter(t, "80mm") s := p.NewSession("test", nil) s.Write([]byte("linea\n\x1dV\x00")) if got := len(p.Tickets()); got != 0 { t.Fatalf("con una sola línea bajo la cuchilla no debe salir ticket, salen %d", got) } s.Write([]byte("\x1dVA\x00")) if got := len(p.Tickets()); got != 1 { t.Fatalf("tickets = %d, want 1", got) } } func TestCommandSplitAcrossWrites(t *testing.T) { p := newTestPrinter(t, "80mm") s := p.NewSession("test", nil) img := []byte{0x1d, 'v', '0', 0, 2, 0, 3, 0, 0xff, 0xff, 0x81, 0x81, 0xff, 0xff} for _, c := range img { s.Write([]byte{c}) } s.Write([]byte("\x1dVA\x00")) if blackPixels(p) != 16+4+16 { t.Fatalf("píxeles negros = %d, want 36", blackPixels(p)) } } func TestRealtimeStatusWhileCoverOpen(t *testing.T) { p := newTestPrinter(t, "80mm") var r recorder s := p.NewSession("test", r.reply) s.Write([]byte{dle, 0x04, 1}) if got := r.take(); !bytes.Equal(got, []byte{0x12}) { t.Fatalf("DLE EOT 1 online = % X, want 12", got) } p.UpdateStatus(func(st *Status) { st.CoverOpen = true }) s.Write([]byte("retenido\n")) s.Write([]byte{dle, 0x04, 2}) if got := r.take(); !bytes.Equal(got, []byte{0x12 | 0x04 | 0x40}) { t.Fatalf("DLE EOT 2 con tapa abierta = % X, want 56", got) } if p.Snapshot(0).PendingBytes == 0 { t.Fatal("los datos deberían seguir en el buffer") } p.UpdateStatus(func(st *Status) { st.CoverOpen = false }) if p.Snapshot(0).PendingBytes != 0 { t.Fatal("al cerrar la tapa se debería imprimir el buffer") } } func TestGSI(t *testing.T) { p := newTestPrinter(t, "80mm") var r recorder s := p.NewSession("test", r.reply) s.Write([]byte{gs, 'I', 67}) if got, want := r.take(), []byte("_ESCPOS-EMU-80\x00"); !bytes.Equal(got, want) { t.Fatalf("GS I 67 = %q, want %q", got, want) } p58 := newTestPrinter(t, "58mm") s58 := p58.NewSession("test", r.reply) s58.Write([]byte{gs, 'I', 67}) if got := r.take(); len(got) != 0 { t.Fatalf("el perfil 58mm no debería responder a GS I 67, responde %q", got) } } func TestQRAndDrawer(t *testing.T) { p := newTestPrinter(t, "80mm") var r recorder s := p.NewSession("test", r.reply) data := "https://example.com" n := len(data) + 3 cmd := []byte{gs, '(', 'k', byte(n), byte(n >> 8), 49, 80, 48} cmd = append(cmd, data...) cmd = append(cmd, gs, '(', 'k', 3, 0, 49, 67, 4) cmd = append(cmd, gs, '(', 'k', 3, 0, 49, 81, 48) cmd = append(cmd, esc, 'p', 0, 25, 250) cmd = append(cmd, dle, 0x04, 1) cmd = append(cmd, gs, 'V', 'A', 0) s.Write(cmd) if blackPixels(p) < 500 { t.Fatalf("el QR no parece impreso (%d píxeles negros)", blackPixels(p)) } if got := r.take(); !bytes.Equal(got, []byte{0x16}) { t.Fatalf("DLE EOT 1 con cajón abierto = % X, want 16", got) } } // TestSampleReceipt genera un ticket de ejemplo. Con ESCPOS_PNG=dir se // guarda como PNG para inspeccionarlo a ojo. func TestSampleReceipt(t *testing.T) { p := newTestPrinter(t, "80mm") s := p.NewSession("test", nil) var b bytes.Buffer b.WriteString("\x1b@\x1bt\x13\x1ba\x01\x1d!\x11TIENDA\n\x1d!\x00") b.WriteString("\x1bE\x01Negrita\x1bE\x00 normal \x1b-\x01subrayado\x1b-\x00\n") b.WriteString("\x1ba\x00Ca\xa4a 1,50 \xd5\n") // PC858: ñ y € b.WriteString("\x1dB\x01 INVERTIDO \x1dB\x00\x1bM\x01 fuente B\x1bM\x00\n") b.WriteString("\x1ba\x02Total: 12,00\n\x1ba\x00") b.WriteString("Col1\tCol2\tCol3\n") b.WriteString("\x1dVB\x10") s.Write(b.Bytes()) tix := p.Tickets() if len(tix) != 1 { t.Fatalf("tickets = %d, want 1", len(tix)) } if dir := os.Getenv("ESCPOS_PNG"); dir != "" { f, err := os.Create(filepath.Join(dir, "sample.png")) if err != nil { t.Fatal(err) } defer f.Close() if err := png.Encode(f, tix[0].Image); err != nil { t.Fatal(err) } } }