1package e2e23import (4 "strings"5 "testing"67 ghostty "go.mitchellh.com/libghostty"8)910// spending types a small table of two series with labels and headers.11func spending(s *session) {12 s.keys("Month", "<tab>", "Rent", "<tab>", "Food", "<enter>")13 s.keys("Jan", "<tab>", "$1,450", "<tab>", "$600", "<enter>")14 s.keys("Feb", "<tab>", "$1,450", "<tab>", "$640", "<enter>")15 s.keys("Mar", "<tab>", "$1,500", "<tab>", "$612", "<enter>")16 s.keys("Apr", "<tab>", "$1,500", "<tab>", "$700", "<enter>")17 s.keys("<ctrl+home>")18 s.waitForBar("A1", "Month")19}2021// insertChart charts the table around A1 from the Insert menu and waits22// for the editor.23func insertChart(s *session) {24 s.keys("<alt+i>")25 s.waitFor("│ Chart")26 s.keys("<up>", "<up>", "<up>", "<enter>") // past Dropdown and Checkbox, at the end27 s.waitFor("Series in columns")28}2930func TestInsertEditMoveDeleteChart(t *testing.T) {31 t.Parallel()32 dir := t.TempDir()33 s := start(t, dir)34 spending(s)35 insertChart(s)36 s.waitFor("Rent and Food")37 s.waitFor("$1,500 ┤")38 s.waitFor("■ Rent ■ Food")39 if !strings.Contains(s.line(0), "CHART") {40 t.Errorf("indicator: %q", s.line(0))41 }4243 // Line chart, then keep it: the chart stays selected.44 s.keys("<right>", "<right>", "<enter>")45 s.waitFor("Line chart of A1:C5")46 // Arrows move it down a row; Esc deselects.47 if !strings.Contains(s.line(gridRow1), "┌─ Rent and Food") {48 t.Fatalf("chart not at D1:\n%s", s.screen())49 }50 s.keys("<down>", "<esc>")51 s.waitFor("READY")52 s.eventually("chart moved", func() bool { return strings.Contains(s.line(gridRow1+1), "┌─ Rent and Food") })5354 // The chart follows its data.55 s.keys("<f5>", "B3", "<enter>", "3000", "<enter>")56 s.waitFor("$3,000 ┤")5758 // Saved with the sheet and back on reopening.59 s.keys("<ctrl+s>", "charts", "<enter>")60 s.eventually("saved", func() bool { return s.title() == "012 - charts.012" })61 s.keys("<ctrl+q>")62 s.waitExit()63 r := start(t, dir, "charts.012")64 r.waitFor("Rent and Food")65 r.waitFor("$3,000 ┤")6667 // Click the chart to select it; Del deletes it and Ctrl+Z restores it.68 r.click(ghostty.MouseButtonLeft, 50, 10)69 r.waitFor("Line chart of A1:C5")70 r.keys("<delete>")71 r.waitFor("Deleted the chart")72 if strings.Contains(r.screen(), "Rent and Food") {73 t.Error("chart still shown after Del")74 }75 r.keys("<ctrl+z>")76 r.waitFor("Rent and Food")77}7879func TestDragChart(t *testing.T) {80 t.Parallel()81 s := start(t, "")82 spending(s)83 insertChart(s)84 s.keys("<enter>", "<esc>")85 s.waitFor("READY")86 // The chart's top-left corner is at D1: x 6+30, y 4. Drag its body87 // two rows down and one column right.88 s.mouse(ghostty.MouseActionPress, ghostty.MouseButtonLeft, 45, 8, 0)89 s.mouse(ghostty.MouseActionMotion, ghostty.MouseButtonLeft, 55, 10, 0)90 s.mouse(ghostty.MouseActionRelease, ghostty.MouseButtonLeft, 55, 10, 0)91 s.eventually("chart at E3", func() bool {92 l := s.line(gridRow1 + 2)93 return len(l) > 46 && strings.HasPrefix(l[46:], "┌─ Rent and Food")94 })95}9697func TestKittyImages(t *testing.T) {98 t.Parallel()99 s := startWith(t, options{graphics: true})100 spending(s)101 insertChart(s)102 s.keys("<enter>")103 // The plot is Unicode placeholders naming image 16, and the image104 // is stored with a virtual placement.105 s.eventually("placeholders", func() bool { return strings.ContainsRune(s.screen(), '\U0010EEEE') })106 s.eventually("image 16", func() bool {107 s.mu.Lock()108 defer s.mu.Unlock()109 kg, err := s.vt.KittyGraphics()110 if err != nil {111 return false112 }113 img := kg.Image(16)114 if img == nil {115 return false116 }117 w, _ := img.Width()118 h, _ := img.Height()119 it, err := ghostty.NewKittyGraphicsPlacementIterator()120 if err != nil {121 return false122 }123 defer it.Close()124 kg.PlacementIterator(it)125 for it.Next() {126 id, _ := it.ImageID()127 virtual, _ := it.IsVirtual()128 if id == 16 && virtual && w > 0 && h > 0 {129 return true130 }131 }132 return false133 })134 // Labels and legend stay text.135 s.waitFor("$1,500 ┤")136 s.waitFor("■ Rent ■ Food")137 // Deleting the chart frees the image.138 s.keys("<delete>")139 s.eventually("image freed", func() bool {140 s.mu.Lock()141 defer s.mu.Unlock()142 kg, _ := s.vt.KittyGraphics()143 return kg.Image(16) == nil144 })145}146147func TestLinksAndErrorMarks(t *testing.T) {148 t.Parallel()149 s := start(t, "")150 s.keys("https://example.com/docs", "<enter>", `=HYPERLINK("example.org", "Example")`, "<enter>", "=1/0", "<enter>", "<up>")151 s.waitFor("#DIV/0! Division by zero in 1/0")152 s.mu.Lock()153 defer s.mu.Unlock()154 uri := func(x, y int) string {155 ref, err := s.vt.GridRef(ghostty.Point{Tag: ghostty.PointTagActive, X: uint16(x), Y: uint32(y)})156 if err != nil {157 t.Fatal(err)158 }159 u, _ := ref.HyperlinkURI()160 return u161 }162 if got := uri(8, gridRow1); got != "https://example.com/docs" {163 t.Errorf("A1 links to %q", got)164 }165 if got := uri(8, gridRow1+1); got != "https://example.org" {166 t.Errorf("A2 links to %q", got)167 }168 if got := uri(8, gridRow1+2); got != "" {169 t.Errorf("error cell links to %q", got)170 }171 ref, _ := s.vt.GridRef(ghostty.Point{Tag: ghostty.PointTagActive, X: 9, Y: gridRow1 + 2})172 if st, err := ref.Style(); err != nil || st.Underline() != ghostty.UnderlineCurly {173 t.Errorf("error cell underline %v, want curly", st.Underline())174 }175}176177// TestChartOptions stacks a column chart, fixes its axis minimum, moves178// its legend, saves and reopens it, and turns it into a scatter with a179// trend line.180func TestChartOptions(t *testing.T) {181 t.Parallel()182 dir := t.TempDir()183 s := start(t, dir)184 spending(s)185 insertChart(s)186 s.keys("k")187 s.waitFor("Stacked")188 s.waitFor("$2,000 ┤")189 s.keys("a")190 s.waitFor("Min auto")191 s.keys("n")192 s.waitFor("Axis minimum (blank for auto):")193 s.keys("1000", "<enter>")194 s.waitFor("Min 1000")195 s.waitFor("$1,000 ┼")196 s.keys("p")197 s.waitFor("Legend right")198 s.waitFor("■ Rent")199 s.keys("<esc>")200 s.waitFor("Series in columns")201 s.keys("<enter>")202 s.waitFor("Column chart of A1:C5")203 s.keys("<esc>", "<ctrl+s>", "options", "<enter>")204 s.eventually("saved", func() bool { return s.title() == "012 - options.012" })205 s.keys("<ctrl+q>")206 s.waitExit()207208 r := start(t, dir, "options.012")209 r.waitFor("$1,000 ┼")210 r.waitFor("■ Rent")211 r.click(ghostty.MouseButtonLeft, 50, 10)212 r.waitFor("Column chart of A1:C5")213 r.keys("<enter>")214 r.waitFor("Stacked")215 r.keys("6", "e")216 r.waitFor("Trend line")217 r.keys("<enter>")218 r.waitFor("Scatter chart of A1:C5")219}