package braille import ( "bytes" "context" "errors" "io" "strings" "testing" ) func TestViewportUnicodePanningAndReplacement(t *testing.T) { v := viewport{width: 4} v.replace("é界abcde") if got := v.frame(); got != "é界ab" { t.Fatalf("first frame: %q", got) } if v.pan(PanLeft) { t.Fatal("panned before beginning") } if !v.pan(PanRight) || v.frame() != "cde " { t.Fatalf("second frame: %q", v.frame()) } if v.pan(PanRight) { t.Fatal("panned past end") } if !v.pan(PanLeft) || v.frame() != "é界ab" { t.Fatal("could not return to first frame") } v.pan(PanRight) v.replace("x") if v.offset != 0 || v.frame() != "x " { t.Fatalf("replacement retained old cells: %q", v.frame()) } v.replace("") if v.frame() != " " { t.Fatal("empty frame was not cleared") } v.replace("\x00\n\t\x7f") if v.frame() != " " { t.Fatal("control characters reached display") } } func TestViewportWordWrapAndIndicators(t *testing.T) { v := viewport{width: 6} if err := v.setFrame(Frame{Text: "one two three", Cursor: -1, WordWrap: true, Dots: []byte{0, 0, 0, 0, 0x40, 0x40, 0, 0, 0, 0, 0, 0, 0}}); err != nil { t.Fatal(err) } if v.frame() != "one " { t.Fatalf("first wrapped frame=%q", v.frame()) } if !v.pan(PanRight) || v.frame() != "two " || v.offset != 4 || v.displayDots()[0] != 0x40 { t.Fatalf("wrapped frame=%q offset=%d dots=%v", v.frame(), v.offset, v.displayDots()) } if !v.pan(PanLeft) || v.offset != 0 { t.Fatalf("left offset=%d", v.offset) } if err := v.setFrame(Frame{Text: "abcdefghij", Cursor: -1, WordWrap: true}); err != nil { t.Fatal(err) } if !v.pan(PanRight) || v.offset != 6 { t.Fatalf("long word offset=%d", v.offset) } } func TestViewportCursorFollowsCaretAndPanning(t *testing.T) { v := viewport{width: 4} if err := v.setFrame(Frame{Text: "é界abcde", Cursor: 5}); err != nil { t.Fatal(err) } if v.frame() != "cde " || v.displayCursor() != 2 { t.Fatalf("caret page: frame=%q cursor=%d", v.frame(), v.displayCursor()) } if !v.pan(PanLeft) || v.displayCursor() != 0 { t.Fatal("cursor remained visible outside the panned viewport") } if !v.pan(PanRight) || v.displayCursor() != 2 { t.Fatal("cursor did not reappear on its page") } if err := v.setFrame(Frame{Text: "abcd", Cursor: 4}); err != nil { t.Fatal(err) } if v.frame() != " " || v.displayCursor() != 1 { t.Fatalf("end-of-line cell: frame=%q cursor=%d", v.frame(), v.displayCursor()) } for _, cursor := range []int{-2, 5} { if err := v.setFrame(Frame{Text: "abcd", Cursor: cursor}); err == nil { t.Fatalf("accepted cursor %d", cursor) } } } func TestGeometry(t *testing.T) { for _, g := range []Geometry{{0, 1}, {20, 0}, {-1, 1}, {4097, 1}, {1, 4097}, {4096, 4096}} { if _, err := g.Cells(); err == nil { t.Errorf("accepted %+v", g) } } if cells, err := (Geometry{20, 2}).Cells(); err != nil || cells != 40 { t.Fatalf("multirow: %d, %v", cells, err) } } type fakeBackend struct { geometry Geometry openErr, writeErr, readErr, closeErr error opens, closes int writes []string cursors []int keys []Key cancel context.CancelFunc } func (f *fakeBackend) Open() (Geometry, error) { f.opens++; return f.geometry, f.openErr } func (f *fakeBackend) Write(text string, cursor int) error { f.writes = append(f.writes, text) f.cursors = append(f.cursors, cursor) return f.writeErr } func (f *fakeBackend) ReadKey() (Key, error) { if len(f.keys) > 0 { key := f.keys[0] f.keys = f.keys[1:] return key, f.readErr } if f.cancel != nil { f.cancel() } return NoKey, f.readErr } func (f *fakeBackend) Close() error { f.closes++; return f.closeErr } func TestDiagnosticPansAndReleases(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() f := &fakeBackend{geometry: Geometry{20, 1}, keys: []Key{PanRight, PanLeft}, cancel: cancel} var output bytes.Buffer if err := Diagnostic(ctx, f, &output); err != nil { t.Fatal(err) } if f.opens != 1 || f.closes != 1 || len(f.writes) != 3 { t.Fatalf("lifecycle: %+v", f) } if f.writes[0] != f.writes[2] || f.writes[0] == f.writes[1] { t.Fatalf("pan frames: %q", f.writes) } if !strings.Contains(output.String(), "20 columns, 1 rows") { t.Fatal(output.String()) } } func TestDiagnosticFailureCleanup(t *testing.T) { failure := errors.New("fake failure") for _, stage := range []string{"open", "geometry", "write", "read", "close"} { t.Run(stage, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() f := &fakeBackend{geometry: Geometry{20, 1}, cancel: cancel} switch stage { case "open": f.openErr = failure case "geometry": f.geometry = Geometry{} case "write": f.writeErr = failure case "read": f.readErr = failure case "close": f.closeErr = failure } err := Diagnostic(ctx, f, io.Discard) if err == nil { t.Fatal("failure was lost") } if stage != "geometry" && !errors.Is(err, failure) { t.Fatalf("lost cause: %v", err) } if f.closes != 1 { t.Fatalf("close count: %d", f.closes) } if (stage == "open" || stage == "geometry") && len(f.writes) > 0 { t.Fatal("wrote without a valid display") } }) } } func TestDiagnosticCanceledBeforeOpen(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() f := &fakeBackend{} if err := Diagnostic(ctx, f, io.Discard); !errors.Is(err, context.Canceled) { t.Fatal(err) } if f.opens != 0 { t.Fatal("opened after cancellation") } } func TestViewportRoutingRejectsPaddingAndTracksReplacement(t *testing.T) { v := viewport{width: 4} if err := v.setFrame(Frame{ID: 1, Text: "é猫abc", Cursor: -1}); err != nil { t.Fatal(err) } v.pan(PanRight) if got, ok := v.route(RouteBase); !ok || got != (RoutingEvent{ID: 1, Offset: 4}) { t.Fatalf("route %+v %v", got, ok) } for _, key := range []Key{PanLeft, RouteBase + 1, RouteBase + 4, RouteBase - 1} { if _, ok := v.route(key); ok { t.Fatalf("routed invalid cell %d", key) } } if err := v.setFrame(Frame{ID: 2, Text: "new", Cursor: -1}); err != nil { t.Fatal(err) } if got, ok := v.route(RouteBase + 1); !ok || got != (RoutingEvent{ID: 2, Offset: 1}) { t.Fatalf("replacement route %+v %v", got, ok) } }