From 7e07ea31cc58c70ae7ed5a9526ee2574dbe4aac1 Mon Sep 17 00:00:00 2001 From: Levi Date: Fri, 18 Sep 2026 00:31:06 +0800 Subject: [PATCH] test(webview): cover event stream failures and cleanup --- internal/webview/rpcprovider/events_test.go | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/internal/webview/rpcprovider/events_test.go b/internal/webview/rpcprovider/events_test.go index 8311e392..61b12092 100644 --- a/internal/webview/rpcprovider/events_test.go +++ b/internal/webview/rpcprovider/events_test.go @@ -2,9 +2,11 @@ package rpcprovider import ( "encoding/json" + "io" "net/http" "net/http/httptest" "strings" + "sync/atomic" "testing" "time" @@ -85,3 +87,121 @@ func TestPageEventStream(t *testing.T) { t.Fatal("registration after close") } } + +func TestPageEventStreamHandshakeFailureCanRetry(t *testing.T) { + for _, tc := range []struct { + name string + status int + body string + }{ + {"http_error", http.StatusUnauthorized, `{"ready":true}`}, + {"invalid_json", http.StatusOK, `not json`}, + {"not_ready", http.StatusOK, `{"ready":false}`}, + } { + t.Run(tc.name, func(t *testing.T) { + var requests atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if requests.Add(1) == 1 { + w.WriteHeader(tc.status) + _, _ = io.WriteString(w, tc.body) + return + } + _, _ = io.WriteString(w, "{\"ready\":true}\n{\"event\":\"load\",\"data\":{\"url\":\"https://example.test\"}}\n") + w.(http.Flusher).Flush() + <-r.Context().Done() + })) + defer server.Close() + p := &page{client: NewClient(wv.RPCConfig{Network: "tcp", Address: strings.TrimPrefix(server.URL, "http://")}), id: "retry-page"} + if off, err := p.On("load", func(wv.Event) { t.Error("failed registration retained its listener") }); err == nil || off != nil { + t.Fatal("failed handshake accepted") + } + got := make(chan wv.Event, 1) + off, err := p.On("load", func(e wv.Event) { got <- e }) + if err != nil { + t.Fatalf("retry: %v", err) + } + defer func() { + p.eventCancel() + <-p.eventDone + }() + defer off() + select { + case e := <-got: + if e.Data["url"] != "https://example.test" { + t.Fatal(e) + } + case <-time.After(time.Second): + t.Fatal("retry did not receive load notification") + } + if requests.Load() != 2 { + t.Fatalf("unexpected request count: %d", requests.Load()) + } + }) + } +} + +func TestPageEventStreamDisconnectAndClose(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request wv.RPCRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Error(err) + return + } + switch request.Method { + case wv.MethodPageEvents: + // End the stream without a terminal event, as if the host dropped it. + _, _ = io.WriteString(w, "{\"ready\":true}\n") + case wv.MethodPageClose: + _ = json.NewEncoder(w).Encode(wv.RPCResponse{}) + default: + t.Errorf("unexpected method: %s", request.Method) + } + })) + defer server.Close() + p := &page{client: NewClient(wv.RPCConfig{Network: "tcp", Address: strings.TrimPrefix(server.URL, "http://")}), id: "disconnected-page"} + closed := make(chan wv.Event, 1) + if _, err := p.On("closed", func(e wv.Event) { closed <- e }); err != nil { + t.Fatal(err) + } + select { + case <-p.eventDone: + case <-time.After(time.Second): + t.Fatal("disconnected stream did not stop") + } + if _, err := p.On("load", func(wv.Event) {}); err == nil || !strings.Contains(err.Error(), "disconnected") { + t.Fatalf("registration hid stream failure: %v", err) + } + if err := p.Close(); err != nil { + t.Fatal(err) + } + select { + case event := <-closed: + if event.Data["reason"] != "api" { + t.Fatal(event) + } + default: + t.Fatal("API close did not release listeners after disconnect") + } + if err := p.Close(); err != nil { + t.Fatal(err) + } + select { + case event := <-closed: + t.Fatalf("duplicate close notification: %+v", event) + default: + } +} + +func TestPageEventStreamConnectionFailureCanRetry(t *testing.T) { + server := httptest.NewServer(http.NotFoundHandler()) + server.Close() + p := &page{client: NewClient(wv.RPCConfig{Network: "tcp", Address: strings.TrimPrefix(server.URL, "http://")}), id: "offline-page"} + for i := 0; i < 2; i++ { + if off, err := p.On("load", func(wv.Event) {}); err == nil || off != nil { + t.Fatal("registration to unavailable host succeeded") + } + } + if p.eventCancel != nil { + t.Fatal("failed connection retained an active stream") + } +}