Data validation follows the named ranges in the Data menu and Checkbox and Dropdown follow Chart in the Insert menu; tests reaching items by letter or from the end of a menu step past them. The rules panel says how rules combine: in order for conditional formats, one per cell for validation.
- commit
- 87ad1175b6575cdb184746ee099f880d2d7ba66e
- parent
- 83650221725fa24a63adca746fec1ae890c173d2
- author
- Dami <dami.etoile@gmail.com>
| M | e2e/ | 2 | +- |
| M | e2e/ | 2 | +- |
| A | e2e/ | 120 | ++++++++++++++++++++++++++++++ |
| M | e2e/ | 2 | +- |
| M | internal/ | 2 | +- |
| M | internal/ | 4 | +- |
| M | internal/ | 13 | ++- |
| M | internal/ | 13 | ++-- |
@@ -24,7 +24,7 @@ func TestNamedRanges(t *testing.T) {
2424 s := start(t, "")2525 s.keys("10", "<enter>", "20", "<enter>", "=SUM(Pair)", "<enter>")2626 s.waitFor("#NAME?")27- s.keys("<ctrl+home>", "<shift+down>", "<alt+d>", "d")27+ s.keys("<ctrl+home>", "<shift+down>", "<alt+d>", "d", "<enter>")2828 s.waitFor("Name for A1:A2:")2929 s.keys("Pair", "<enter>")3030 s.waitForName("Pair")@@ -0,0 +1,120 @@
1+package e2e2+3+import (4+ "os"5+ "path/filepath"6+ "testing"7+)8+9+// tasksBook is a task list with rules on every column: owners from a10+// dropdown (one of them not on its list), hours on a color scale, done11+// tasks struck through in green, and checkboxes.12+const tasksBook = `{13+ "version": 2,14+ "widths": {"A": 14, "B": 10, "C": 8, "D": 7},15+ "cells": {16+ "A1": {"input":"Task","bold":true},17+ "B1": {"input":"Owner","bold":true},18+ "C1": {"input":"Hours","bold":true,"align":"right"},19+ "D1": {"input":"Done","bold":true,"align":"center"},20+ "A2": "Write spec", "B2": "Ann", "C2": "12", "D2": "TRUE",21+ "A3": "Build UI", "B3": "Bo", "C3": "40", "D3": "FALSE",22+ "A4": "Tests", "B4": "Cy", "C4": "26", "D4": "TRUE",23+ "A5": "Docs", "B5": "Dee", "C5": "6",24+ "A6": "Release", "B6": "Ann", "C6": "3", "D6": "FALSE",25+ "A7": "Total", "C7": "=SUM(C2:C6)"26+ },27+ "conditionalFormats": [28+ {"ranges":"A2:A6","condition":"formula","values":["=$D2"],"text":"green","strikethrough":true},29+ {"ranges":"C2:C6","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]},30+ {"ranges":"C7","condition":"gt","values":["80"],"fill":"magenta","bold":true}31+ ],32+ "validations": [33+ {"ranges":"B2:B6","criteria":"list","items":["Ann","Bo","Cy"],"help":"Pick who does it"},34+ {"ranges":"D2:D6","criteria":"checkbox"},35+ {"ranges":"C2:C6","criteria":"number","condition":"between","values":["0","80"],"reject":true}36+ ]37+}38+`39+40+func tasksFile(t *testing.T, dir string) {41+ if err := os.WriteFile(filepath.Join(dir, "tasks.012"), []byte(tasksBook), 0o644); err != nil {42+ t.Fatal(err)43+ }44+}45+46+// openTasks opens the task list.47+func openTasks(s *session) {48+ s.keys("<ctrl+o>")49+ s.waitFor("Open file:")50+ s.keys("tasks", "<enter>")51+ s.waitFor("Write spec")52+}53+54+var ruleScreens = []screen{55+ {name: "rules-sheet", files: tasksFile, setup: func(s *session) {56+ openTasks(s)57+ s.keys("<down>", "<down>", "<down>", "<down>", "<right>")58+ s.waitFor("Invalid: Pick who does it")59+ }},60+ {name: "rules-sheet-dracula", opts: options{config: "theme = Dracula\n"}, files: tasksFile, setup: func(s *session) {61+ openTasks(s)62+ s.keys("<down>", "<down>", "<right>", "<right>", "<right>")63+ s.waitFor("check or uncheck")64+ }},65+ {name: "rules-sheet-latte", opts: options{light: true, config: "theme = Catppuccin Latte\n"}, files: tasksFile, setup: func(s *session) {66+ openTasks(s)67+ s.keys("<down>", "<down>", "<right>", "<right>", "<right>")68+ s.waitFor("check or uncheck")69+ }},70+ {name: "rules-panel", files: tasksFile, setup: func(s *session) {71+ openTasks(s)72+ s.keys("<ctrl+k>", "conditional formatting", "<enter>")73+ s.waitFor("Conditional format rules")74+ s.keys("<down>", "<down>")75+ s.waitFor("first rule that matches")76+ }},77+ {name: "rules-editor", files: tasksFile, setup: func(s *session) {78+ openTasks(s)79+ s.keys("<ctrl+k>", "conditional formatting", "<enter>")80+ s.waitFor("Conditional format rules")81+ s.keys("<down>", "<enter>")82+ s.waitFor("Edit conditional format")83+ }},84+ {name: "rules-editor-narrow", opts: options{cols: 60, rows: 16}, files: tasksFile, setup: func(s *session) {85+ openTasks(s)86+ s.keys("<ctrl+k>", "conditional formatting", "<enter>")87+ s.waitFor("Conditional format rules")88+ s.keys("<down>", "<down>", "<enter>")89+ s.waitFor("Minpoint")90+ }},91+ {name: "validation-editor", files: tasksFile, setup: func(s *session) {92+ openTasks(s)93+ s.keys("<ctrl+k>", "data validation", "<enter>")94+ s.waitFor("Data validation rules")95+ s.keys("<down>", "<enter>")96+ s.waitFor("Edit data validation")97+ }},98+ {name: "dropdown-open", files: tasksFile, setup: func(s *session) {99+ openTasks(s)100+ s.keys("<down>", "<down>", "<right>", "<alt+down>")101+ s.waitFor("Search the items")102+ }},103+ {name: "validation-reject", files: tasksFile, setup: func(s *session) {104+ openTasks(s)105+ s.keys("<down>", "<right>", "<right>", "120", "<enter>")106+ s.waitFor("Invalid entry in C2")107+ }},108+}109+110+func init() {111+ light := map[string]bool{"rules-sheet": true, "rules-panel": true, "rules-editor": true, "validation-editor": true, "dropdown-open": true}112+ for _, sc := range ruleScreens {113+ screens = append(screens, sc)114+ if light[sc.name] {115+ sc.name += "-light"116+ sc.opts.light = true117+ screens = append(screens, sc)118+ }119+ }120+}@@ -101,7 +101,7 @@ func pivotEditor(s *session) {
101101 // name, ending on the total.102102 func named(s *session) {103103 budget(s)104- s.keys("<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>", "<alt+d>", "d", "Expenses", "<enter>")104+ s.keys("<up>", "<up>", "<up>", "<up>", "<shift+down>", "<shift+down>", "<alt+d>", "d", "<enter>", "Expenses", "<enter>")105105 s.waitForName("Expenses")106106 s.keys("<esc>", "<ctrl+home>", "<down>", "<right>", "<right>", "4", "<enter>")107107 s.keys("<ctrl+home>", "<down>", "<down>", "<down>", "<down>", "<down>", "<down>", "<down>", "<right>")@@ -96,9 +96,9 @@ var menuBar = []menuDef{
9696 {cmd: "data.sort_range", title: "Advanced range sorting options"},9797 }}, sep,9898 {cmd: "data.filter"}, {cmd: "data.filter_column"}, {cmd: "data.filter_remove"}, sep,99- {cmd: "data.validation"}, {cmd: "data.validation_clear"}, sep,10099 {cmd: "data.pivot"}, {cmd: "data.pivot_edit"}, {cmd: "data.frequency"}, sep,101100 {cmd: "data.named_ranges"}, {cmd: "data.define_name"}, sep,101+ {cmd: "data.validation"}, {cmd: "data.validation_clear"}, sep,102102 {cmd: "data.precedents"}, {cmd: "data.dependents"}, sep,103103 {title: "Macros", items: macroItems}, sep,104104 {cmd: "jev.refresh"},@@ -13,7 +13,9 @@ type cfKind struct{}
1313 func (cfKind) title() string { return "Conditional format rules" }1414 func (cfKind) count(s *sheet.Sheet) int { return len(s.CondFormats()) }1515 func (cfKind) remove(s *sheet.Sheet, i int) { s.DeleteCondFormat(i) }16-func (cfKind) addHint() string { return "A" }16+17+func (cfKind) ordered() bool { return true }18+func (cfKind) listHint() string { return "The first rule that matches a cell wins" }1719 1820 func (cfKind) move(s *sheet.Sheet, i, to int) bool {1921 s.MoveCondFormat(i, to)@@ -51,7 +51,10 @@ type kind interface {
5151 remove(s *sheet.Sheet, i int)5252 // move moves rule i to to, and reports whether rules have an order.5353 move(s *sheet.Sheet, i, to int) bool54- addHint() string54+ // ordered reports whether the rules' order matters, and listHint55+ // says how rules combine, for the status line.56+ ordered() bool57+ listHint() string5558 }5659 5760 // form is a rule being added or edited.@@ -311,7 +314,7 @@ func (e *Editor) listLines(inner int) []string {
311314 base = th.MenuSelected312315 }313316 if i == 0 {314- lines = append(lines, spread(base, base.Render(" + Add rule"), th.Muted.Inherit(base).Render(e.k.addHint()+" "), inner))317+ lines = append(lines, theme.Cells(base, " + Add rule", inner))315318 continue316319 }317320 lines = append(lines, e.k.item(th, e.h, i-1, inner, i == e.list.Sel))@@ -348,8 +351,10 @@ func (e *Editor) Status() (string, string) {
348351 case e.f == nil && e.list.Sel == 0:349352 desc, pairs = "Add a rule over the selection", []string{"Enter", "add", "Esc", "close"}350353 case e.f == nil:351- desc, pairs = "Rules apply in order: the first that matches a cell formats it",352- []string{"Enter", "edit", "Del", "remove", "Shift+↑/↓", "move", "Esc", "close"}354+ desc, pairs = e.k.listHint(), []string{"Enter", "edit", "Del", "remove", "Esc", "close"}355+ if e.k.ordered() {356+ pairs = []string{"Enter", "edit", "Del", "remove", "Shift+↑/↓", "move", "Esc", "close"}357+ }353358 default:354359 r := e.rows()[e.sel]355360 desc = r.hint@@ -13,13 +13,14 @@ var errRange = errors.New("Enter the range the rule applies to, e.g. B2:B100")
1313 // dvKind lists data validation rules.1414 type dvKind struct{}1515 16-func (dvKind) title() string { return "Data validation rules" }17-func (dvKind) count(s *sheet.Sheet) int { return len(s.Validations()) }18-func (dvKind) remove(s *sheet.Sheet, i int) { s.DeleteValidation(i) }19-func (dvKind) move(*sheet.Sheet, int, int) bool {20- return false // a cell has one rule, so they have no order16+func (dvKind) title() string { return "Data validation rules" }17+func (dvKind) count(s *sheet.Sheet) int { return len(s.Validations()) }18+func (dvKind) remove(s *sheet.Sheet, i int) { s.DeleteValidation(i) }19+func (dvKind) move(*sheet.Sheet, int, int) bool { return false }20+func (dvKind) ordered() bool { return false }21+func (dvKind) listHint() string {22+ return "A cell has one rule: a new rule takes its cells from the others"2123 }22-func (dvKind) addHint() string { return "A" }2324 2425 // marks are how the list shows each kind of rule, as the cells do.2526 var marks = map[sheet.ValidKind]string{sheet.ValidList: " ▾ ", sheet.ValidRange: " ▾ ", sheet.ValidCheckbox: "[✓]"}