返回 DeepSeek-Reasonix
live_test.go
根目录 / internal / browser / cdp / live_test.go
1 package cdp
2
3 import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "os"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/browser"
13 )
14
15 // livePage exercises the snapshot walker and every input path the executor
16 // dispatches: a labelled text box, a submit button that reports what it got,
17 // a select, and a link the walker must name.
18 const livePage = `<!doctype html>
19 <html><body>
20 <h1>Live fixture</h1>
21 <form id="f" onsubmit="event.preventDefault(); document.getElementById('out').textContent = 'submitted:' + q.value + ':' + pick.value;">
22 <label for="q">Query</label>
23 <input id="q" name="q" type="text" placeholder="Search">
24 <select id="pick" name="pick"><option value="one">One</option><option value="two">Two</option></select>
25 <button type="submit">Run search</button>
26 </form>
27 <input id="secret" type="password" value="hunter2">
28 <a href="https://example.test/docs">Docs</a>
29 <div id="out"></div>
30 <div id="hidden" style="display:none"><button>Never</button></div>
31 </body></html>`
32
33 // TestLiveChrome drives a real browser. It stays skipped in normal CI because
34 // it needs a Chrome install; it is the only test that runs the injected
35 // isolated-world helper against a real DOM.
36 //
37 // Run with:
38 //
39 // REASONIX_LIVE_CHROME=1 go test ./internal/browser/cdp \
40 // -run '^TestLiveChrome$' -v -count=1 -timeout=3m
41 func TestLiveChrome(t *testing.T) {
42 if os.Getenv("REASONIX_LIVE_CHROME") != "1" {
43 t.Skip("set REASONIX_LIVE_CHROME=1 to run the real Chrome end-to-end test")
44 }
45 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46 w.Header().Set("Content-Type", "text/html; charset=utf-8")
47 _, _ = w.Write([]byte(livePage))
48 }))
49 defer srv.Close()
50
51 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
52 defer cancel()
53 exec, err := New(ctx, Options{Headless: true, ArtifactDir: t.TempDir(), NavigateTimeout: 30 * time.Second})
54 if err != nil {
55 t.Fatalf("launch chrome: %v", err)
56 }
57 defer exec.Shutdown()
58
59 tab, err := exec.Open(ctx, browser.OpenRequest{OperationID: "live-open", URL: srv.URL})
60 if err != nil {
61 t.Fatalf("open: %v", err)
62 }
63 snap, err := exec.Snapshot(ctx, browser.SnapshotRequest{TabID: tab.ID})
64 if err != nil {
65 t.Fatalf("snapshot: %v", err)
66 }
67 t.Logf("snapshot refs=%d\n%s", snap.Refs, snap.Tree)
68 for _, want := range []string{`textbox "Query"`, `button "Run search"`, `link "Docs"`, "combobox"} {
69 if !strings.Contains(snap.Tree, want) {
70 t.Errorf("snapshot tree is missing %s:\n%s", want, snap.Tree)
71 }
72 }
73 if strings.Contains(snap.Tree, "hunter2") {
74 t.Errorf("the snapshot leaked a password field's value:\n%s", snap.Tree)
75 }
76 if strings.Contains(snap.Tree, `"Never"`) {
77 t.Errorf("the snapshot walked a display:none subtree:\n%s", snap.Tree)
78 }
79
80 queryRef := refFor(t, snap.Tree, `textbox "Query"`)
81 pickRef := refFor(t, snap.Tree, "combobox")
82 buttonRef := refFor(t, snap.Tree, `button "Run search"`)
83
84 act(t, ctx, exec, browser.ActRequest{
85 OperationID: "live-type", TabID: tab.ID, DocumentToken: snap.DocumentToken,
86 Action: browser.ActionType, Ref: queryRef, Text: "reasonix",
87 })
88 act(t, ctx, exec, browser.ActRequest{
89 OperationID: "live-select", TabID: tab.ID, DocumentToken: snap.DocumentToken,
90 Action: browser.ActionSelect, Ref: pickRef, Options: []string{"Two"},
91 })
92 act(t, ctx, exec, browser.ActRequest{
93 OperationID: "live-click", TabID: tab.ID, DocumentToken: snap.DocumentToken,
94 Action: browser.ActionClick, Ref: buttonRef,
95 })
96
97 after, err := exec.Snapshot(ctx, browser.SnapshotRequest{TabID: tab.ID, Selector: "#out"})
98 if err != nil {
99 t.Fatalf("snapshot after acting: %v", err)
100 }
101 if !strings.Contains(after.Tree, "submitted:reasonix:two") {
102 t.Fatalf("the page did not observe the typed text, the selection, and the click:\n%s", after.Tree)
103 }
104 if after.DocumentToken == snap.DocumentToken {
105 t.Fatal("a second snapshot reused the first token")
106 }
107 // The retired token must not be usable even though the document never left.
108 if _, err := exec.Act(ctx, browser.ActRequest{
109 OperationID: "live-stale", TabID: tab.ID, DocumentToken: snap.DocumentToken,
110 Action: browser.ActionClick, Ref: buttonRef,
111 }); err == nil {
112 t.Fatal("a write against the retired token was accepted")
113 }
114
115 shot, err := exec.Screenshot(ctx, browser.ScreenshotRequest{TabID: tab.ID, FullPage: true})
116 if err != nil {
117 t.Fatalf("screenshot: %v", err)
118 }
119 if shot.Width == 0 || shot.Height == 0 {
120 t.Fatalf("screenshot has no size: %+v", shot)
121 }
122 if err := exec.Close(ctx, browser.CloseRequest{OperationID: "live-close", TabID: tab.ID}); err != nil {
123 t.Fatalf("close: %v", err)
124 }
125 }
126
127 func act(t *testing.T, ctx context.Context, exec *Executor, req browser.ActRequest) {
128 t.Helper()
129 res, err := exec.Act(ctx, req)
130 if err != nil {
131 t.Fatalf("%s %s: %v", req.Action, req.Ref, err)
132 }
133 if !res.Executed {
134 t.Fatalf("%s %s was not executed: %s", req.Action, req.Ref, res.Reason)
135 }
136 }
137
138 // refFor returns the ref of the first snapshot line containing want.
139 func refFor(t *testing.T, tree, want string) string {
140 t.Helper()
141 for line := range strings.SplitSeq(tree, "\n") {
142 if !strings.Contains(line, want) {
143 continue
144 }
145 _, rest, ok := strings.Cut(line, "[ref=")
146 if !ok {
147 continue
148 }
149 if ref, _, ok := strings.Cut(rest, "]"); ok {
150 return ref
151 }
152 }
153 t.Fatalf("no ref for %q in:\n%s", want, tree)
154 return ""
155 }
156
156 lines GO