package braille import ( "bytes" "context" "io" "strings" "testing" "time" ) type clientBackend struct { fakeBackend cancel context.CancelFunc } func (backend *clientBackend) Write(text string, cursor int) error { if err := backend.fakeBackend.Write(text, cursor); err != nil { return err } backend.cancel() return nil } func TestRunClientShowsLiveFrameAndReleasesDisplay(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() backend := &clientBackend{fakeBackend: fakeBackend{geometry: Geometry{20, 1}}, cancel: cancel} input := strings.NewReader("{\"text\":\"Save, push button\",\"cursor\":-1}\n") var ready bytes.Buffer if err := RunClient(ctx, backend, io.NopCloser(input), &ready); err != nil { t.Fatal(err) } if ready.String() != "ready\n" || len(backend.writes) != 1 || backend.writes[0] != "Save, push button " || backend.opens != 1 || backend.closes != 1 { t.Fatalf("client lifecycle: ready=%q backend=%+v", ready.String(), backend) } } func TestRunClientPlacesCursorOnUnicodeCaret(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() backend := &clientBackend{fakeBackend: fakeBackend{geometry: Geometry{4, 1}}, cancel: cancel} input := strings.NewReader("{\"text\":\"é界abcde\",\"cursor\":5}\n") if err := RunClient(ctx, backend, io.NopCloser(input), &bytes.Buffer{}); err != nil { t.Fatal(err) } if len(backend.writes) != 1 || backend.writes[0] != "cde " || len(backend.cursors) != 1 || backend.cursors[0] != 2 { t.Fatalf("writes=%q cursors=%v", backend.writes, backend.cursors) } } func TestRunClientRejectsInvalidCursorAndCloses(t *testing.T) { backend := &fakeBackend{geometry: Geometry{4, 1}} err := RunClient(context.Background(), backend, io.NopCloser(strings.NewReader("{\"text\":\"abc\",\"cursor\":4}\n")), &bytes.Buffer{}) if err == nil || backend.opens != 1 || backend.closes != 1 || len(backend.writes) != 0 { t.Fatalf("error=%v backend=%+v", err, backend) } } func TestOfferLatestFrameReplacesPendingFrame(t *testing.T) { frames := make(chan Frame, 1) offerLatestFrame(frames, Frame{Text: "old"}) offerLatestFrame(frames, Frame{Text: "new"}) if frame := <-frames; frame.Text != "new" { t.Fatalf("retained %q", frame.Text) } offerLatestFrame(frames, Frame{Text: "newer"}) if frame := <-frames; frame.Text != "newer" { t.Fatalf("lost empty-queue update: %q", frame.Text) } } type routingBackend struct{ fakeBackend } func (b *routingBackend) ReadKey() (Key, error) { if len(b.writes) == 0 { return NoKey, nil } return b.fakeBackend.ReadKey() } func TestRunClientRoutesDisplayedFrameAfterPanningAndJoinsReader(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() input, writer := io.Pipe() defer writer.Close() backend := &routingBackend{fakeBackend{geometry: Geometry{4, 1}, keys: []Key{PanRight, RouteBase + 1}, cancel: cancel}} var output bytes.Buffer done := make(chan error, 1) go func() { done <- RunClient(ctx, backend, input, &output) }() if _, err := io.WriteString(writer, "{\"text\":\"é猫abcdef\",\"cursor\":-1,\"id\":42}\n"); err != nil { t.Fatal(err) } if err := <-done; err != nil { t.Fatal(err) } if output.String() != "ready\n{\"id\":42,\"offset\":5}\n" { t.Fatalf("helper output %q", output.String()) } if backend.closes != 1 { t.Fatalf("close count %d", backend.closes) } // The writer has deliberately remained open; return requires the session to // close the blocked input reader and join it. if _, err := writer.Write([]byte("x")); err == nil { t.Fatal("reader still open") } }