-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexecargs_e2e_test.go
More file actions
360 lines (317 loc) · 10.9 KB
/
execargs_e2e_test.go
File metadata and controls
360 lines (317 loc) · 10.9 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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
//go:build !ci
package tinkerdown_test
import (
"fmt"
"strings"
"testing"
"time"
"net/http/httptest"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/livetemplate/tinkerdown/internal/config"
"github.com/livetemplate/tinkerdown/internal/server"
)
// TestExecArgsForm tests the auto-generated argument form functionality.
// This test verifies:
// 1. Form inputs are rendered for each argument
// 2. Input types match the inferred types (text, number, checkbox)
// 3. Default values are populated
// 4. --help descriptions are shown
// 5. Form submission triggers Run action
// 6. Output reflects the submitted values
func TestExecArgsForm(t *testing.T) {
// Enable exec sources for this test (disabled by default for security)
config.SetAllowExec(true)
defer config.SetAllowExec(false)
// Load config from test example
cfg, err := config.LoadFromDir("examples/exec-args-test")
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
// Verify source is configured correctly
if cfg.Sources == nil {
t.Fatal("No sources configured in tinkerdown.yaml")
}
source, ok := cfg.Sources["greeting"]
if !ok {
t.Fatal("greeting source not found in config")
}
if source.Type != "exec" {
t.Fatalf("Expected exec source type, got: %s", source.Type)
}
if !source.Manual {
t.Fatal("Expected manual: true for greeting source")
}
t.Logf("Source config: type=%s, cmd=%s, manual=%t", source.Type, source.Cmd, source.Manual)
// Create test server
srv := server.NewWithConfig("examples/exec-args-test", cfg)
if err := srv.Discover(); err != nil {
t.Fatalf("Failed to discover pages: %v", err)
}
handler := server.WithCompression(srv)
ts := httptest.NewServer(handler)
defer ts.Close()
// Setup Docker Chrome for reliable CI execution
chromeCtx, cleanup := SetupDockerChrome(t, 60*time.Second)
defer cleanup()
ctx := chromeCtx.Context
// Store console logs for debugging
var consoleLogs []string
chromedp.ListenTarget(ctx, func(ev interface{}) {
if ev, ok := ev.(*runtime.EventConsoleAPICalled); ok {
for _, arg := range ev.Args {
consoleLogs = append(consoleLogs, fmt.Sprintf("[Console] %s", arg.Value))
}
}
})
// Convert URL for Docker Chrome access
url := ConvertURLForDockerChrome(ts.URL)
t.Logf("Test server URL: %s (Docker: %s)", ts.URL, url)
// Test 1: Navigate and wait for page to load
var hasInteractiveBlock bool
err = chromedp.Run(ctx,
chromedp.Navigate(url+"/"),
chromedp.Sleep(3*time.Second),
chromedp.Evaluate(`document.querySelector('.tinkerdown-interactive-block') !== null`, &hasInteractiveBlock),
)
if err != nil {
t.Fatalf("Failed to navigate: %v", err)
}
if !hasInteractiveBlock {
var htmlContent string
chromedp.Run(ctx, chromedp.OuterHTML("html", &htmlContent))
t.Logf("HTML: %s", htmlContent[:min(2000, len(htmlContent))])
t.Fatal("Page did not load correctly - no interactive block found")
}
t.Log("Page loaded with interactive block")
// Wait for form to be rendered (may take a moment after WebSocket connection)
var hasForm bool
var inputCount int
for i := 0; i < 20; i++ {
err = chromedp.Run(ctx,
chromedp.Evaluate(`document.querySelector('form[name="Run"]') !== null`, &hasForm),
)
if err == nil && hasForm {
break
}
time.Sleep(300 * time.Millisecond)
}
err = chromedp.Run(ctx,
chromedp.Evaluate(`document.querySelectorAll('form[name="Run"] input').length`, &inputCount),
)
if err != nil {
t.Fatalf("Failed to check form: %v", err)
}
if !hasForm {
var htmlContent string
chromedp.Run(ctx, chromedp.OuterHTML("html", &htmlContent))
// Get more of the content
t.Logf("HTML length: %d", len(htmlContent))
t.Logf("HTML: %s", htmlContent[:min(5000, len(htmlContent))])
t.Logf("Console logs: %v", consoleLogs)
t.Fatal("Form with name='Run' not found")
}
t.Log("Form with name='Run' found")
// We expect 3 inputs: name (text), count (number), uppercase (checkbox)
if inputCount < 3 {
var htmlContent string
chromedp.Run(ctx, chromedp.OuterHTML("html", &htmlContent))
t.Logf("HTML: %s", htmlContent[:min(3000, len(htmlContent))])
t.Fatalf("Expected at least 3 form inputs, got: %d", inputCount)
}
t.Logf("Found %d form inputs", inputCount)
// Test 3: Verify input types are correct
var nameInputType string
var countInputType string
var uppercaseInputType string
err = chromedp.Run(ctx,
chromedp.Evaluate(`document.querySelector('input[name="name"]')?.type || 'not found'`, &nameInputType),
chromedp.Evaluate(`document.querySelector('input[name="count"]')?.type || 'not found'`, &countInputType),
chromedp.Evaluate(`document.querySelector('input[name="uppercase"]')?.type || 'not found'`, &uppercaseInputType),
)
if err != nil {
t.Fatalf("Failed to check input types: %v", err)
}
if nameInputType != "text" {
t.Fatalf("Expected name input type 'text', got: '%s'", nameInputType)
}
t.Log("Name input is text type")
if countInputType != "number" {
t.Fatalf("Expected count input type 'number', got: '%s'", countInputType)
}
t.Log("Count input is number type")
if uppercaseInputType != "checkbox" {
t.Fatalf("Expected uppercase input type 'checkbox', got: '%s'", uppercaseInputType)
}
t.Log("Uppercase input is checkbox type")
// Test 4: Verify default values
var nameValue string
var countValue string
var uppercaseChecked bool
err = chromedp.Run(ctx,
chromedp.Evaluate(`document.querySelector('input[name="name"]')?.value || ''`, &nameValue),
chromedp.Evaluate(`document.querySelector('input[name="count"]')?.value || ''`, &countValue),
chromedp.Evaluate(`document.querySelector('input[name="uppercase"]')?.checked || false`, &uppercaseChecked),
)
if err != nil {
t.Fatalf("Failed to check default values: %v", err)
}
if nameValue != "World" {
t.Fatalf("Expected name default 'World', got: '%s'", nameValue)
}
t.Logf("Name default value: %s", nameValue)
if countValue != "3" {
t.Fatalf("Expected count default '3', got: '%s'", countValue)
}
t.Logf("Count default value: %s", countValue)
if uppercaseChecked {
t.Fatal("Expected uppercase to be unchecked (false)")
}
t.Log("Uppercase default is unchecked")
// Test 5: Verify --help descriptions are shown (if introspection worked)
var hasDescriptions bool
err = chromedp.Run(ctx,
chromedp.Evaluate(`document.body.textContent.includes('The name to greet')`, &hasDescriptions),
)
if err != nil {
t.Fatalf("Failed to check descriptions: %v", err)
}
if hasDescriptions {
t.Log("Descriptions from --help are displayed")
} else {
t.Log("Note: Descriptions from --help not found (introspection may have failed, which is ok)")
}
// Wait for WebSocket connection before submitting form
var isConnected bool
for i := 0; i < 20; i++ {
chromedp.Run(ctx, chromedp.Evaluate(`
(() => {
return window.tinkerdown && window.tinkerdown._client && window.tinkerdown._client.isConnected ? true : false;
})()
`, &isConnected))
if isConnected {
break
}
time.Sleep(200 * time.Millisecond)
}
if !isConnected {
// Fallback: check console logs
for _, log := range consoleLogs {
if strings.Contains(log, "[Livemdtools] Connected") {
isConnected = true
break
}
}
}
if !isConnected {
time.Sleep(2 * time.Second)
}
t.Log("WebSocket connection established")
// Test 6: Modify values and submit form
err = chromedp.Run(ctx,
// Clear name field and enter new value
chromedp.Clear(`input[name="name"]`, chromedp.ByQuery),
chromedp.SendKeys(`input[name="name"]`, "Alice", chromedp.ByQuery),
// Clear count field and enter new value
chromedp.Clear(`input[name="count"]`, chromedp.ByQuery),
chromedp.SendKeys(`input[name="count"]`, "2", chromedp.ByQuery),
// Check the uppercase checkbox
chromedp.Click(`input[name="uppercase"]`, chromedp.ByQuery),
)
if err != nil {
t.Fatalf("Failed to modify form values: %v", err)
}
t.Log("Modified form values")
// Submit the form
err = chromedp.Run(ctx,
chromedp.Click(`form[name="Run"] button[type="submit"]`, chromedp.ByQuery),
)
if err != nil {
t.Fatalf("Failed to submit form: %v", err)
}
t.Log("Form submitted")
// Wait for command to update with new values (DOM update via morphdom)
var commandText string
for i := 0; i < 30; i++ {
err = chromedp.Run(ctx,
chromedp.Evaluate(`
(() => {
// Get all code elements and their text
const codes = document.querySelectorAll('code');
const texts = Array.from(codes).map(c => c.textContent);
// Also check if the main block's innerHTML contains Alice
const main = document.querySelector('main[lvt-source="greeting"]');
const mainHtml = main ? main.innerHTML.substring(0, 500) : 'main not found';
return JSON.stringify({codes: texts, mainHtml: mainHtml});
})()
`, &commandText),
)
if err == nil && strings.Contains(commandText, "Alice") {
break
}
time.Sleep(200 * time.Millisecond)
}
// Parse the debug info
t.Logf("DOM state after wait: %s", commandText)
// The DOM might have multiple code elements due to morphdom updates
// Check if any of them contains the updated command
if !strings.Contains(commandText, "Alice") {
t.Logf("Console logs: %v", consoleLogs)
t.Fatal("DOM does not contain updated name 'Alice'")
}
t.Log("Command updated with new name value")
if !strings.Contains(commandText, "count 2") && !strings.Contains(commandText, "count2") {
t.Fatal("DOM does not contain updated count '2'")
}
t.Log("Command updated with new count value")
if !strings.Contains(commandText, "uppercase true") {
t.Fatal("DOM does not contain updated uppercase 'true'")
}
t.Log("Command updated with uppercase=true")
// Test 8: Verify output shows correct data
var hasAliceMessage bool
var hasUppercaseMessage bool
err = chromedp.Run(ctx,
chromedp.Evaluate(`document.body.innerHTML.includes('HELLO, ALICE')`, &hasUppercaseMessage),
chromedp.Evaluate(`document.body.innerHTML.includes('Alice')`, &hasAliceMessage),
)
if err != nil {
t.Fatalf("Failed to check output: %v", err)
}
if !hasAliceMessage && !hasUppercaseMessage {
var htmlContent string
chromedp.Run(ctx, chromedp.OuterHTML("html", &htmlContent))
t.Logf("HTML: %s", htmlContent[:min(3000, len(htmlContent))])
t.Fatal("Output does not contain greeting for Alice")
}
if hasUppercaseMessage {
t.Log("Output shows uppercase greeting for Alice")
} else {
t.Log("Output shows greeting for Alice")
}
// Test 9: Verify status is success
var statusText string
err = chromedp.Run(ctx,
chromedp.Evaluate(`
(() => {
// Look for status in the template output
const el = document.body;
if (el && el.textContent.includes('success')) {
return 'success';
}
return el ? el.textContent.substring(0, 200) : '';
})()
`, &statusText),
)
if err != nil {
t.Fatalf("Failed to check status: %v", err)
}
if !strings.Contains(statusText, "success") {
var htmlContent string
chromedp.Run(ctx, chromedp.OuterHTML("html", &htmlContent))
t.Logf("HTML: %s", htmlContent[:min(3000, len(htmlContent))])
t.Fatalf("Status is not success: %s", statusText)
}
t.Log("Status shows success")
t.Log("All exec args form tests passed!")
}