1package ui23import (4 "strings"5 "testing"67 "src.rest/cat/012/internal/sheet"8 "src.rest/cat/012/internal/ui/picker"9 "src.rest/cat/012/internal/ui/rowtext"10 "src.rest/cat/012/internal/ui/rules"11)1213func mustValidate(t *testing.T, m *Model, line string) {14 t.Helper()15 v, err := sheet.ParseValidation(line)16 if err != nil {17 t.Fatal(err)18 }19 if err := m.sheet.AddValidation(v); err != nil {20 t.Fatal(err)21 }22}2324func TestConditionalFormattingPanel(t *testing.T) {25 m := newModel()26 press(t, m, "5", "<enter>", "150", "<enter>", "<up>", "<up>", "<shift+down>")27 run(m, m.runCommand("format.conditional"))28 if _, ok := m.overlay.(*rules.Editor); !ok || !strings.Contains(screen(m), "Conditional format rules") {29 t.Fatalf("overlay %T:\n%s", m.overlay, screen(m))30 }31 if !strings.Contains(line(m, menuLine), "RULES") {32 t.Errorf("indicator: %q", line(m, menuLine))33 }34 press(t, m, "<enter>", "<down>", "<down>")35 for range 9 {36 press(t, m, "<right>") // Greater than37 }38 press(t, m, "<down>", "100", "<enter>")39 fs := m.sheet.CondFormats()40 if len(fs) != 1 || fs[0].Summary() != "Greater than 100" || sheet.RangesText(fs[0].Ranges) != "A1:A2" || !m.changed {41 t.Fatalf("rules %+v", fs)42 }43 press(t, m, "<esc>")44 if m.overlay != nil || m.mode != modeReady {45 t.Fatalf("still open: %T", m.overlay)46 }47 // A2 is drawn on the green fill, A1 isn't.48 sp := rowtext.Span{Text: "150", Owner: 0}49 _, shade, shaded := m.ruleSpan(addr("A2"), &sp, false, m.slotColor)50 if !shaded || shade.Style.GetBackground() != m.th.RuleFill[sheet.ColorGreen].GetBackground() {51 t.Errorf("A2 shade %v shaded %v", shade.Style.GetBackground(), shaded)52 }53 if _, _, shaded := m.ruleSpan(addr("A1"), &sp, false, m.slotColor); shaded {54 t.Error("A1 formatted")55 }56 if _, _, shaded := m.ruleSpan(addr("A2"), &sp, true, m.slotColor); shaded {57 t.Error("the pointer's colors gave way to a rule's")58 }59 press(t, m, "<ctrl+z>")60 if len(m.sheet.CondFormats()) != 0 {61 t.Error("undo kept the rule")62 }63}6465func TestCheckboxes(t *testing.T) {66 m := newModel()67 press(t, m, "<right>", "<shift+down>", "<shift+down>")68 run(m, m.runCommand("insert.checkbox"))69 if vs := m.sheet.Validations(); len(vs) != 1 || vs[0].Kind != sheet.ValidCheckbox {70 t.Fatalf("validations %+v", vs)71 }72 press(t, m, "<esc>", "<space>")73 if input(m, "B1") != "TRUE" || m.mode != modeReady {74 t.Fatalf("B1 %q mode %v", input(m, "B1"), m.mode)75 }76 out := screen(m)77 if strings.Count(out, "[✓]") != 1 || strings.Count(out, "[ ]") != 2 {78 t.Errorf("glyphs:\n%s", out)79 }80 if !strings.Contains(line(m, contextLine), "Space") {81 t.Errorf("context line %q", line(m, contextLine))82 }83 // A selection toggles together, as the active cell goes.84 press(t, m, "<shift+down>", "<shift+down>", "<space>")85 if input(m, "B1") != "FALSE" || input(m, "B3") != "" { // blank is unchecked already86 t.Errorf("B1 %q B3 %q", input(m, "B1"), input(m, "B3"))87 }88 press(t, m, "<space>")89 if input(m, "B1") != "TRUE" || input(m, "B3") != "TRUE" || m.sheet.Book().UndoLabel() != "check B1:B3" {90 t.Errorf("check all: B1 %q B3 %q %q", input(m, "B1"), input(m, "B3"), m.sheet.Book().UndoLabel())91 }92 press(t, m, "<space>")93 if input(m, "B2") != "FALSE" {94 t.Errorf("uncheck all: B2 %q", input(m, "B2"))95 }96 press(t, m, "<ctrl+z>", "<ctrl+z>")97 // A click on the glyph toggles; Space elsewhere starts an entry.98 press(t, m, "<esc>")99 click(m, minRowHdrW+sheet.DefaultWidth+4, gridTop+1, 0)100 if m.cur != addr("B2") || input(m, "B2") != "TRUE" {101 t.Errorf("click: cur %v B2 %q", m.cur, input(m, "B2"))102 }103 press(t, m, "<left>", " ")104 if m.mode != modeEnter {105 t.Errorf("space on a plain cell: mode %v", m.mode)106 }107}108109func TestDropdown(t *testing.T) {110 m := newModel()111 mustValidate(t, m, `{"ranges":"A1:A5","criteria":"list","items":["Yes","No","Maybe"]}`)112 if !strings.Contains(line(m, gridTop), "▾") {113 t.Errorf("no dropdown mark: %q", line(m, gridTop))114 }115 press(t, m, "<alt+down>")116 p, ok := m.overlay.(*picker.Picker)117 if !ok || len(p.Items) != 3 || p.At == nil {118 t.Fatalf("overlay %T", m.overlay)119 }120 press(t, m, "no", "<enter>")121 if input(m, "A1") != "No" || m.overlay != nil {122 t.Fatalf("A1 %q overlay %T", input(m, "A1"), m.overlay)123 }124 // Clicking the mark of another cell opens its list there.125 click(m, minRowHdrW+sheet.DefaultWidth-1, gridTop+2, 0)126 if _, ok := m.overlay.(*picker.Picker); !ok || m.cur != addr("A3") {127 t.Fatalf("click: cur %v overlay %T", m.cur, m.overlay)128 }129 press(t, m, "<esc>")130 if input(m, "A3") != "" {131 t.Error("Esc entered something")132 }133}134135func TestValidationRejectsAndWarns(t *testing.T) {136 m := newModel()137 mustValidate(t, m, `{"ranges":"A1:A9","criteria":"number","condition":"between","values":["1","10"],"reject":true}`)138 mustValidate(t, m, `{"ranges":"B1:B9","criteria":"number","condition":"between","values":["1","10"]}`)139 press(t, m, "42", "<enter>")140 if m.mode != modeEdit || input(m, "A1") != "" || !strings.Contains(line(m, contextLine), "Invalid entry in A1: Input must be a number between 1 and 10") {141 t.Fatalf("reject: mode %v A1 %q %q", m.mode, input(m, "A1"), line(m, contextLine))142 }143 press(t, m, "<esc>", "<right>", "42", "<enter>")144 if input(m, "B1") != "42" || !strings.Contains(line(m, contextLine), "Invalid entry in B1") {145 t.Fatalf("warn: B1 %q %q", input(m, "B1"), line(m, contextLine))146 }147 press(t, m, "<up>")148 if !strings.Contains(line(m, contextLine), "Invalid: Input must be a number between 1 and 10") || !m.sheet.Look(addr("B1")).Invalid {149 t.Errorf("on the invalid cell: %q", line(m, contextLine))150 }151 // Scripts are refused too.152 script(t, m, `select("A2")153enter("0")`)154 if input(m, "A2") != "" || !strings.Contains(m.warn, "Input must be a number between 1 and 10") {155 t.Errorf("script: A2 %q warn %q", input(m, "A2"), m.warn)156 }157}158159func TestRuleMacros(t *testing.T) {160 m := newModel()161 press(t, m, "<shift+down>")162 src := recordRules(t, m)163 for _, want := range []string{`run("format.conditional_add", answer={"ranges": "A1:A2", "condition": "not_empty", "fill": "green"})`,164 `run("format.conditional_remove", answer=1)`,165 `run("insert.checkbox")`, `run("data.validation_add", answer="{\"ranges\":\"C1:C2\",\"criteria\":\"list\",\"items\":[\"a\",\"b\"]}")`} {166 if !strings.Contains(src, want) {167 t.Errorf("script lacks %s:\n%s", want, src)168 }169 }170 fresh := newModel()171 script(t, fresh, src)172 if len(fresh.sheet.CondFormats()) != 1 || len(fresh.sheet.Validations()) != 2 {173 t.Errorf("replay: %+v %+v %q", fresh.sheet.CondFormats(), fresh.sheet.Validations(), fresh.warn)174 }175}176177// recordRules records adding a conditional format from the panel, a178// checkbox and a dropdown from the Add command.179func recordRules(t *testing.T, m *Model) string {180 t.Helper()181 send(m, nil)182 run(m, m.runCommand("macro.record"))183 run(m, m.runCommand("format.conditional"))184 press(t, m, "<enter>", "<enter>") // add the rule the form starts with185 press(t, m, "<down>", "<delete>", "<esc>") // removed186 run(m, m.runCommand("format.conditional"))187 press(t, m, "<enter>", "<enter>", "<esc>", "<right>")188 run(m, m.runCommand("insert.checkbox"))189 press(t, m, "<right>")190 run(m, m.runCommand("data.validation_add"))191 m.line.Set(`{"ranges":"C1:C2","criteria":"list","items":["a","b"]}`)192 press(t, m, "<enter>")193 run(m, m.runCommand("macro.stop"))194 m.line.Set("Rules")195 press(t, m, "<enter>", "<enter>")196 mc, ok := m.book().Macro("Rules")197 if !ok {198 t.Fatalf("no macro: %q", m.errMsg)199 }200 return mc.Source201}202203func TestRulesRespectPivots(t *testing.T) {204 m := wideModel()205 salesTable(t, m)206 m.runCommand("data.pivot")207 press(t, m, "<space>", "reg", "<enter>", "<enter>")208 press(t, m, "<down>")209 run(m, m.runCommand("insert.checkbox"))210 if len(m.sheet.Validations()) != 0 || !strings.Contains(line(m, contextLine), "Pivot table results can't be edited") {211 t.Errorf("checkbox on a pivot: %+v %q", m.sheet.Validations(), line(m, contextLine))212 }213 if err := m.host().SaveValidation(-1, sheet.Validation{Ranges: []sheet.Rect{rectOf("A1:B3")}, Kind: sheet.ValidCheckbox}); err == nil {214 t.Error("validation saved over a pivot")215 }216 // Conditional formats only draw, so they may color a pivot.217 if err := m.host().SaveFormat(-1, sheet.CondFormat{Ranges: []sheet.Rect{rectOf("B2:B3")}, Op: sheet.RuleGreater, Args: [2]string{"5"}, Style: sheet.RuleStyle{Bold: true}}); err != nil {218 t.Error(err)219 }220}221222// Data bars, icon sets and the rules that compare cells are recorded223// from the panel as the other rules are, and replay the same.224func TestRecordBarsAndIcons(t *testing.T) {225 m := newModel()226 press(t, m, "<shift+down>")227 send(m, nil)228 run(m, m.runCommand("macro.record"))229 run(m, m.runCommand("format.conditional"))230 press(t, m, "<enter>", "<down>", "<right>", "<right>", "<enter>") // a data bar231 press(t, m, "<enter>", "<down>", "<right>", "<right>", "<right>", "<enter>") // an icon set232 press(t, m, "<esc>")233 for _, line := range []string{234 `{"ranges":"B1:B9","condition":"top_percent","values":["10"],"fill":"green"}`,235 `{"ranges":"C1:C9","condition":"date_is","values":["last month"],"bold":true}`,236 } {237 run(m, m.runCommand("format.conditional_add"))238 m.line.Set(line)239 press(t, m, "<enter>")240 }241 run(m, m.runCommand("macro.stop"))242 m.line.Set("Bars")243 press(t, m, "<enter>", "<enter>")244 mc, ok := m.book().Macro("Bars")245 if !ok {246 t.Fatalf("no macro: %q", m.errMsg)247 }248 if !strings.Contains(mc.Source, `"dataBar": {`) || !strings.Contains(mc.Source, `"iconSet": {`) {249 t.Errorf("script:\n%s", mc.Source)250 }251 fresh := newModel()252 script(t, fresh, mc.Source)253 want, got := m.sheet.CondFormats(), fresh.sheet.CondFormats()254 if len(got) != 4 || len(want) != 4 {255 t.Fatalf("replay: %d rules, want %d; %q", len(got), len(want), fresh.warn)256 }257 for i := range want {258 if got[i].JSON() != want[i].JSON() {259 t.Errorf("rule %d replayed as %s, want %s", i+1, got[i].JSON(), want[i].JSON())260 }261 }262}