-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtui_link_key_test.go
More file actions
305 lines (273 loc) · 10.4 KB
/
Copy pathtui_link_key_test.go
File metadata and controls
305 lines (273 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package main
import (
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/foliagecp/easyjson"
)
// L is the link key, and what it does follows the subject — the same rule as
// every other key. Three states, three actions, each named in the status bar.
func linkModel(t *testing.T) tuiModel {
t.Helper()
fvi := makeVertexInfo("hub/x", nil, nil)
m := makeModel("hub/x", threeLinks(), &fvi)
m.width = 140
return m
}
func TestL_StartsALinkFromTheCentreColumn(t *testing.T) {
m := linkModel(t)
if m.linkKeyAction() != "start link" {
t.Fatalf("action = %q, want start link", m.linkKeyAction())
}
m = update(m, key("L"))
if m.linking == nil || m.linking.fromID != "hub/x" {
t.Fatal("L on the vertex should start a link from it")
}
if !strings.Contains(stripANSI(m.renderStatus()), "commit link") {
t.Error("with a link pending the hint must say what L will do next")
}
}
// TestL_EditsTheSelectedLink is the missing half: selecting a link and pressing
// the link key did nothing at all, which is why L felt like it belonged to
// creation only.
func TestL_EditsTheSelectedLink(t *testing.T) {
m := makeModel("hub/a", []displayLink{rawLink()}, nil)
m.llMode = true // a raw link between plain vertices
m.focus = panelOut
m.rCursor = 1
m = m.applyLinkDetail(linkDetailMsg{
key: keyOf(rawLink()),
detail: linkDetail{loaded: true, body: easyjson.NewJSONObject()},
})
if m.linkKeyAction() != "edit link" {
t.Fatalf("action = %q, want edit link", m.linkKeyAction())
}
m = update(m, key("L"))
if m.form == nil || m.form.kind != formLinkEdit {
t.Fatalf("L over a link should edit it, got %v", m.form)
}
if m.linking != nil {
t.Error("editing a link must not start a new one")
}
}
// TestL_CommitWinsOverEditWhileSomethingIsPending. The banner has been
// promising a commit across every keystroke since it appeared; the cursor
// happening to rest on a link row must not change what the screen said.
func TestL_CommitWinsOverEditWhileSomethingIsPending(t *testing.T) {
m := makeModel("hub/b", threeLinks(), nil)
m.linking = &pendingLink{fromID: "hub/a", kind: vkPlain}
m.focus = panelOut
m.rCursor = 1
if m.linkKeyAction() != "commit link" {
t.Fatalf("action = %q, want commit link", m.linkKeyAction())
}
m = update(m, key("L"))
if m.form == nil || m.form.kind != formLinkCreate {
t.Fatalf("L should commit the pending link, got %v", m.form)
}
}
// TestL_CommitTakesTheBannerDown. A successful commit used to clear nothing,
// so the banner sat there promising a commit that had already happened — and
// every later L meant "commit again" instead of whatever it should have meant.
func TestL_CommitTakesTheBannerDown(t *testing.T) {
withOps(t, graphOps{
linkCreate: func(_, _, _, _ string, _ []string, _ easyjson.JSON, _ bool) opResult {
return opResult{status: opApplied}
},
})
m := makeModel("hub/b", nil, nil)
m = update(m, key("x")) // a raw link needs the low-level API
m.linking = &pendingLink{fromID: "hub/a", kind: vkPlain}
m = update(m, key("L"))
m = focusField(m, "type")
m = typeText(m, "rel")
_, cmd := updateCmd(m, tea_ctrlS())
m = update(m, runCmd(cmd))
if m.linking != nil {
t.Error("the banner must come down once the link exists")
}
if strings.Contains(stripANSI(m.renderStatus()), "LINK PENDING") {
t.Error("the pending banner is still on screen")
}
}
func TestL_AFailedCommitKeepsThePendingLink(t *testing.T) {
withOps(t, graphOps{
linkCreate: func(_, _, _, _ string, _ []string, _ easyjson.JSON, _ bool) opResult {
return opResult{status: opFailed, details: "already exists"}
},
})
m := makeModel("hub/b", nil, nil)
m = update(m, key("x"))
m.linking = &pendingLink{fromID: "hub/a", kind: vkPlain}
m = update(m, key("L"))
m = focusField(m, "type")
m = typeText(m, "rel")
_, cmd := updateCmd(m, tea_ctrlS())
m = update(m, runCmd(cmd))
if m.linking == nil {
t.Error("a failed commit should leave the pending link so it can be retried")
}
}
// ── The tier a link is edited through ─────────────────────────────────────────
// objectsLinkBetween builds the state the browser is in when standing on an
// object with a schema-typed edge to another object: the edge type is the
// SCHEMA's own, so nothing about the edge reveals what is on the other end.
func objectsLinkBetween(t *testing.T) (tuiModel, displayLink) {
t.Helper()
edge := displayLink{info: makeLinkInfo("hub/srv-1", "rack-a", "hub/rack-a", "mounted_in"), isOut: true}
m := makeModel("hub/srv-1", []displayLink{
{info: makeLinkInfo("hub/srv-1", instanceOfLinkName, "hub/srv", ltInstanceOf), isOut: true},
edge,
}, nil)
m.focus = panelOut
return m, edge
}
// TestTier_AnObjectsLinkIsEditableInHighLevelMode is the regression test for
// "I cannot edit links". An objects-link carries the schema's link type, not a
// structural one, so the edge alone cannot say what it connects — and guessing
// "plain" sent every one of them to the raw API, which high-level mode then
// refused. The only editable links left were the structural ones.
func TestTier_AnObjectsLinkIsEditableInHighLevelMode(t *testing.T) {
m, edge := objectsLinkBetween(t)
// Before the far endpoint is known the tier is provisional…
m = m.applyLinkDetail(linkDetailMsg{
key: keyOf(edge),
detail: linkDetail{loaded: true, body: easyjson.NewJSONObject(), farKind: vkObject},
})
if got := m.tierOfSubjectLink(edge); got != tierObjectsLink {
t.Fatalf("tier = %v, want tierObjectsLink once the far end is known", got)
}
if refusal := crudRefusesLink(false, tierObjectsLink); refusal != "" {
t.Errorf("the high-level API owns objects-links: %q", refusal)
}
}
func TestTier_HighLevelStillRefusesEdgesItDoesNotOwn(t *testing.T) {
for _, c := range []struct {
name string
tier linkTier
want bool // refused?
}{
{"raw link between plain vertices", tierRawLink, true},
{"types-link", tierTypesLink, false},
{"objects-link", tierObjectsLink, false},
{"claimed-type object link", tierSuperLink, false},
{"sub-type edge", tierSubType, false},
} {
refusal := crudRefusesLink(false, c.tier)
if (refusal != "") != c.want {
t.Errorf("%s: refusal = %q, want refused=%v", c.name, refusal, c.want)
}
}
// With the low-level API armed nothing is refused — that is the point of it.
for _, tr := range []linkTier{tierRawLink, tierTypesLink, tierObjectsLink} {
if refusal := crudRefusesLink(true, tr); refusal != "" {
t.Errorf("low-level mode refused %v: %q", tr, refusal)
}
}
}
// TestTier_ResolvesTheFarEndpointWhenTheEdgeCannot pins the extra read: it
// happens for a user-typed edge, and not for a structural one.
func TestTier_ResolvesTheFarEndpointWhenTheEdgeCannot(t *testing.T) {
structural := displayLink{info: makeLinkInfo("hub/srv", "rack", "hub/rack", ltInstanceOf), isOut: true}
if _, settled := inferFarKind(structural); !settled {
t.Error("a structural edge says what is on the other end")
}
userTyped := displayLink{info: makeLinkInfo("hub/srv-1", "rack-a", "hub/rack-a", "mounted_in"), isOut: true}
if _, settled := inferFarKind(userTyped); settled {
t.Error("a user-typed edge cannot say what it connects — it must be read")
}
}
// ── Relating two types ────────────────────────────────────────────────────────
// twoTypes stands on a type with another one pending, which is the state L
// commits from.
func twoTypes(t *testing.T) tuiModel {
t.Helper()
m := makeModel("hub/srv", []displayLink{
{info: makeLinkInfo(hubID("types"), "srv", "hub/srv", ltInstanceOf), isOut: false},
}, nil)
m.linking = &pendingLink{fromID: "hub/hw", kind: vkType}
return update(m, key("L"))
}
// TestTypeLink_AsksWhichRelation. Two types can be related in two entirely
// different ways — a types-link is a SCHEMA declaration that permits
// object-links between instances, a sub-type is INHERITANCE — and the form
// used to silently assume the first. Nothing about walking from one type to
// another says which was meant.
func TestTypeLink_AsksWhichRelation(t *testing.T) {
m := twoTypes(t)
if m.form == nil {
t.Fatal("L should open the link form between two types")
}
fl, ok := m.form.field("relation")
if !ok {
t.Fatal("the form must ask which relation this is")
}
labels := ""
for _, o := range fl.options {
labels += o.label + " "
}
if !strings.Contains(labels, "types-link") || !strings.Contains(labels, "sub-type") {
t.Errorf("options = %q, want both relations offered", labels)
}
}
func TestTypeLink_SubTypeSubmitsInheritance(t *testing.T) {
var base, child string
withOps(t, graphOps{
subTypeSet: func(b, c string) opResult { base, child = b, c; return opResult{status: opApplied} },
})
m := twoTypes(t)
m = focusField(m, "relation")
m = update(m, tea.KeyMsg{Type: tea.KeyRight}) // types-link → sub-type
if got := m.form.value("relation"); got != relSubType {
t.Fatalf("relation = %q, want %q", got, relSubType)
}
_, cmd := updateCmd(m, tea_ctrlS())
runCmd(cmd)
if base != "hub/hw" || child != "hub/srv" {
t.Errorf("subTypeSet(%q,%q), want (hub/hw, hub/srv)", base, child)
}
}
func TestTypeLink_TypesLinkStillSubmitsTheSchema(t *testing.T) {
var gotOLT string
withOps(t, graphOps{
typesLinkCreate: func(_, _, olt string, _ []string, _ easyjson.JSON) opResult {
gotOLT = olt
return opResult{status: opApplied}
},
})
m := twoTypes(t)
_, cmd := updateCmd(m, tea_ctrlS())
runCmd(cmd)
if gotOLT != "srv" {
t.Errorf("objectLinkType = %q, want the prefilled target name", gotOLT)
}
}
// TestTypeLink_HidesWhatTheChosenRelationIgnores. A sub-type carries no
// object-link type, no tags and no body — showing them would invite input the
// operation has nowhere to put.
func TestTypeLink_HidesWhatTheChosenRelationIgnores(t *testing.T) {
m := twoTypes(t)
f := *m.form
for _, key := range []string{"olt", "tags", "body"} {
fl, ok := f.field(key)
if !ok {
t.Fatalf("%s should exist for a types-link", key)
}
if !f.visible(fl) {
t.Errorf("%s should be shown while the relation is a types-link", key)
}
}
m = focusField(m, "relation")
m = update(m, tea.KeyMsg{Type: tea.KeyRight})
f = *m.form
for _, key := range []string{"olt", "tags", "body"} {
fl, _ := f.field(key)
if f.visible(fl) {
t.Errorf("%s applies to a types-link, not to a sub-type", key)
}
}
// And the form is submittable even though the hidden `olt` is required.
if !f.ok() {
t.Error("a required field the current choice hides must not block submission")
}
}