From a0cfe1c403913da302eccede729d96142372527c Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 29 Dec 2025 12:07:42 +0100 Subject: [PATCH 1/3] fix: use session scoped URLs for download links, add acceptance tests --- acceptance/dashboard_page.go | 66 +++++++++++++++++++++++ acceptance/dashboard_test.go | 72 ++++++++++++++++++++++++++ dashboard/views/event-details.templ | 8 +-- dashboard/views/event-details_templ.go | 8 +-- dashboard/views/helper.go | 10 ++++ 5 files changed, 156 insertions(+), 8 deletions(-) diff --git a/acceptance/dashboard_page.go b/acceptance/dashboard_page.go index 568f726..aa0a77b 100644 --- a/acceptance/dashboard_page.go +++ b/acceptance/dashboard_page.go @@ -2,6 +2,7 @@ package acceptance import ( "fmt" + "net/url" "testing" "time" @@ -379,3 +380,68 @@ func (dp *DashboardPage) WaitForUsagePanel(timeout float64) { }) require.NoError(dp.t, err, "usage panel did not load content") } + +// BaseURL returns the origin (scheme://host) from the session URL. +func (dp *DashboardPage) BaseURL() string { + dp.t.Helper() + + parsed, err := url.Parse(dp.SessionURL) + require.NoError(dp.t, err) + return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) +} + +// DownloadRequestBody finds the "Download" link in the Request Body section and fetches the content. +// Returns the download URL path (for verification) and the response body content. +func (dp *DashboardPage) DownloadRequestBody() (path string, body []byte, contentType string) { + dp.t.Helper() + + // Find the Request section's Body download link + // Structure: div with h3 "Request" > div with h4 "Body" > a[download] "Download" + link := dp.Page.Locator("#event-details").Locator("h3:has-text('Request')").Locator("..").Locator("h4:has-text('Body')").Locator("..").Locator("a[download]:has-text('Download')") + + href, err := link.GetAttribute("href") + require.NoError(dp.t, err, "failed to get request body download link href") + require.NotEmpty(dp.t, href, "request body download link should have href") + + // Use Playwright's native API request context (shares browser cookies/context) + fullURL := dp.BaseURL() + href + response, err := dp.Page.Request().Get(fullURL) + require.NoError(dp.t, err, "failed to fetch request body download") + require.Equal(dp.t, 200, response.Status(), "request body download should return 200") + + bodyBytes, err := response.Body() + require.NoError(dp.t, err, "failed to read request body") + + headers := response.Headers() + ct := headers["content-type"] + + return href, bodyBytes, ct +} + +// DownloadResponseBody finds the "Download" link in the Response Body section and fetches the content. +// Returns the download URL path (for verification) and the response body content. +func (dp *DashboardPage) DownloadResponseBody() (path string, body []byte, contentType string) { + dp.t.Helper() + + // Find the Response section's Body download link + // Structure: div with h3 "Response" > div with h4 "Body" > a[download] "Download" + link := dp.Page.Locator("#event-details").Locator("h3:has-text('Response')").Locator("..").Locator("h4:has-text('Body')").Locator("..").Locator("a[download]:has-text('Download')") + + href, err := link.GetAttribute("href") + require.NoError(dp.t, err, "failed to get response body download link href") + require.NotEmpty(dp.t, href, "response body download link should have href") + + // Use Playwright's native API request context (shares browser cookies/context) + fullURL := dp.BaseURL() + href + response, err := dp.Page.Request().Get(fullURL) + require.NoError(dp.t, err, "failed to fetch response body download") + require.Equal(dp.t, 200, response.Status(), "response body download should return 200") + + bodyBytes, err := response.Body() + require.NoError(dp.t, err, "failed to read response body") + + headers := response.Headers() + ct := headers["content-type"] + + return href, bodyBytes, ct +} diff --git a/acceptance/dashboard_test.go b/acceptance/dashboard_test.go index 0ca87cb..f223163 100644 --- a/acceptance/dashboard_test.go +++ b/acceptance/dashboard_test.go @@ -231,6 +231,78 @@ func TestNewSessionAfterBrowserClose(t *testing.T) { assert.Equal(t, 0, dashboard2.GetEventCount(), "new session should have no events") } +// TestDownloadRequestBody verifies that request body can be downloaded via the download link. +func TestDownloadRequestBody(t *testing.T) { + t.Parallel() + + app := NewTestApp(t) + defer app.Close() + + pw := NewPlaywrightFixture(t) + defer pw.Close() + + ctx := pw.NewContext(t) + defer ctx.Close() + + dashboard := NewDashboardPage(t, ctx, app.DevlogURL) + dashboard.StartCapture("global") + + // Make a POST request with known body content + dashboard.FetchAPIWithBody("/api/echo", `{"test": "download-data"}`) + dashboard.WaitForEventCount(1, 5000) + + // Click the event to see details + dashboard.ClickFirstEvent() + dashboard.WaitForEventDetails(5000) + + // Download the request body and verify + path, body, contentType := dashboard.DownloadRequestBody() + + // Verify the URL includes the session prefix /s/{sid}/ + assert.Contains(t, path, "/s/", "download URL should include session prefix") + assert.Contains(t, path, "/download/request-body/", "download URL should be for request body") + + // Verify the content type and body content + assert.Contains(t, contentType, "application/json", "content type should be JSON") + assert.Contains(t, string(body), "download-data", "downloaded body should contain original data") +} + +// TestDownloadResponseBody verifies that response body can be downloaded via the download link. +func TestDownloadResponseBody(t *testing.T) { + t.Parallel() + + app := NewTestApp(t) + defer app.Close() + + pw := NewPlaywrightFixture(t) + defer pw.Close() + + ctx := pw.NewContext(t) + defer ctx.Close() + + dashboard := NewDashboardPage(t, ctx, app.DevlogURL) + dashboard.StartCapture("global") + + // Make a POST request - the echo endpoint returns the same body + dashboard.FetchAPIWithBody("/api/echo", `{"response": "body-test"}`) + dashboard.WaitForEventCount(1, 5000) + + // Click the event to see details + dashboard.ClickFirstEvent() + dashboard.WaitForEventDetails(5000) + + // Download the response body and verify + path, body, contentType := dashboard.DownloadResponseBody() + + // Verify the URL structure + assert.Contains(t, path, "/s/", "download URL should include session prefix") + assert.Contains(t, path, "/download/response-body/", "download URL should be for response body") + + // Verify the content type and body content + assert.Contains(t, contentType, "application/json", "content type should be JSON") + assert.Contains(t, string(body), "body-test", "downloaded body should contain original data") +} + // TestUsagePanel verifies that the usage panel shows memory and session stats. func TestUsagePanel(t *testing.T) { t.Parallel() diff --git a/dashboard/views/event-details.templ b/dashboard/views/event-details.templ index d63756a..d839c5e 100644 --- a/dashboard/views/event-details.templ +++ b/dashboard/views/event-details.templ @@ -167,7 +167,7 @@ templ HTTPRequestDetails(event *collector.Event, request collector.HTTPClientReq

Body

@@ -215,7 +215,7 @@ templ HTTPRequestDetails(event *collector.Event, request collector.HTTPClientReq

Body

@@ -314,7 +314,7 @@ templ HTTPServerRequestDetails(event *collector.Event, request collector.HTTPSer

Body

@@ -362,7 +362,7 @@ templ HTTPServerRequestDetails(event *collector.Event, request collector.HTTPSer

Body

diff --git a/dashboard/views/event-details_templ.go b/dashboard/views/event-details_templ.go index f62296f..b0569ba 100644 --- a/dashboard/views/event-details_templ.go +++ b/dashboard/views/event-details_templ.go @@ -464,7 +464,7 @@ func HTTPRequestDetails(event *collector.Event, request collector.HTTPClientRequ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 templ.SafeURL = templ.SafeURL(fmt.Sprintf("%s/download/request-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) + var templ_7745c5c3_Var22 templ.SafeURL = templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadRequestBodyURL(event.ID.String())) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var22))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -537,7 +537,7 @@ func HTTPRequestDetails(event *collector.Event, request collector.HTTPClientRequ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 templ.SafeURL = templ.SafeURL(fmt.Sprintf("%s/download/response-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) + var templ_7745c5c3_Var25 templ.SafeURL = templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadResponseBodyURL(event.ID.String())) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var25))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -768,7 +768,7 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var40 templ.SafeURL = templ.SafeURL(fmt.Sprintf("%s/download/request-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) + var templ_7745c5c3_Var40 templ.SafeURL = templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadRequestBodyURL(event.ID.String())) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var40))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -841,7 +841,7 @@ func HTTPServerRequestDetails(event *collector.Event, request collector.HTTPServ if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var43 templ.SafeURL = templ.SafeURL(fmt.Sprintf("%s/download/response-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) + var templ_7745c5c3_Var43 templ.SafeURL = templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadResponseBodyURL(event.ID.String())) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var43))) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err diff --git a/dashboard/views/helper.go b/dashboard/views/helper.go index b9f835e..03fe727 100644 --- a/dashboard/views/helper.go +++ b/dashboard/views/helper.go @@ -72,6 +72,16 @@ type HandlerOptions struct { CaptureMode string // "session" or "global" } +// BuildDownloadRequestBodyURL builds a URL for downloading the request body of an event +func (opts HandlerOptions) BuildDownloadRequestBodyURL(eventID string) string { + return fmt.Sprintf("%s/s/%s/download/request-body/%s", opts.PathPrefix, opts.SessionID, eventID) +} + +// BuildDownloadResponseBodyURL builds a URL for downloading the response body of an event +func (opts HandlerOptions) BuildDownloadResponseBodyURL(eventID string) string { + return fmt.Sprintf("%s/s/%s/download/response-body/%s", opts.PathPrefix, opts.SessionID, eventID) +} + // BuildEventDetailURL builds a URL for event detail view, preserving capture state func (opts HandlerOptions) BuildEventDetailURL(eventID string) string { base := fmt.Sprintf("%s/s/%s/", opts.PathPrefix, opts.SessionID) From 5ba6d01808d050bb6c1a78df8336829aa0322ecc Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 29 Dec 2025 12:08:38 +0100 Subject: [PATCH 2/3] refactor: use Playwright request instead of fetch for API --- acceptance/dashboard_page.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/acceptance/dashboard_page.go b/acceptance/dashboard_page.go index aa0a77b..791b335 100644 --- a/acceptance/dashboard_page.go +++ b/acceptance/dashboard_page.go @@ -333,26 +333,22 @@ func (dp *DashboardPage) WaitForEventDetails(timeout float64) { require.NoError(dp.t, err, "failed to wait for event details") } -// FetchAPI executes a fetch request from the browser context and waits for the response. +// FetchAPI executes a GET request using Playwright's API request context (shares browser cookies). func (dp *DashboardPage) FetchAPI(path string) { dp.t.Helper() - // Wait for the fetch to complete fully (read the response body) - _, err := dp.Page.Evaluate(fmt.Sprintf(`fetch('%s').then(r => r.text())`, path)) + _, err := dp.Page.Request().Get(dp.BaseURL() + path) require.NoError(dp.t, err) } -// FetchAPIWithBody executes a POST fetch request with JSON body from the browser context. +// FetchAPIWithBody executes a POST request with JSON body using Playwright's API request context. func (dp *DashboardPage) FetchAPIWithBody(path string, body string) { dp.t.Helper() - js := fmt.Sprintf(`fetch('%s', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(%s) - })`, path, body) - - _, err := dp.Page.Evaluate(js) + _, err := dp.Page.Request().Post(dp.BaseURL()+path, playwright.APIRequestContextPostOptions{ + Headers: map[string]string{"Content-Type": "application/json"}, + Data: body, + }) require.NoError(dp.t, err) } From f81d1cbc60fafd0ccbf96f099e10b78fdfb8349f Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 29 Dec 2025 12:16:16 +0100 Subject: [PATCH 3/3] refactor: simplify assertions, assert response body download --- acceptance/dashboard_test.go | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/acceptance/dashboard_test.go b/acceptance/dashboard_test.go index f223163..36dc075 100644 --- a/acceptance/dashboard_test.go +++ b/acceptance/dashboard_test.go @@ -248,23 +248,17 @@ func TestDownloadRequestBody(t *testing.T) { dashboard.StartCapture("global") // Make a POST request with known body content - dashboard.FetchAPIWithBody("/api/echo", `{"test": "download-data"}`) + dashboard.FetchAPIWithBody("/api/echo", `{"test": "request-body-data"}`) dashboard.WaitForEventCount(1, 5000) // Click the event to see details dashboard.ClickFirstEvent() dashboard.WaitForEventDetails(5000) - // Download the request body and verify - path, body, contentType := dashboard.DownloadRequestBody() - - // Verify the URL includes the session prefix /s/{sid}/ - assert.Contains(t, path, "/s/", "download URL should include session prefix") - assert.Contains(t, path, "/download/request-body/", "download URL should be for request body") - - // Verify the content type and body content - assert.Contains(t, contentType, "application/json", "content type should be JSON") - assert.Contains(t, string(body), "download-data", "downloaded body should contain original data") + // Download the request body and verify it contains what we sent + _, body, contentType := dashboard.DownloadRequestBody() + assert.Contains(t, contentType, "application/json") + assert.Contains(t, string(body), "request-body-data") } // TestDownloadResponseBody verifies that response body can be downloaded via the download link. @@ -283,24 +277,18 @@ func TestDownloadResponseBody(t *testing.T) { dashboard := NewDashboardPage(t, ctx, app.DevlogURL) dashboard.StartCapture("global") - // Make a POST request - the echo endpoint returns the same body - dashboard.FetchAPIWithBody("/api/echo", `{"response": "body-test"}`) + // Make a GET request - the /api/test endpoint returns {"status":"ok"} + dashboard.FetchAPI("/api/test") dashboard.WaitForEventCount(1, 5000) // Click the event to see details dashboard.ClickFirstEvent() dashboard.WaitForEventDetails(5000) - // Download the response body and verify - path, body, contentType := dashboard.DownloadResponseBody() - - // Verify the URL structure - assert.Contains(t, path, "/s/", "download URL should include session prefix") - assert.Contains(t, path, "/download/response-body/", "download URL should be for response body") - - // Verify the content type and body content - assert.Contains(t, contentType, "application/json", "content type should be JSON") - assert.Contains(t, string(body), "body-test", "downloaded body should contain original data") + // Download the response body and verify it contains the server's response + _, body, contentType := dashboard.DownloadResponseBody() + assert.Contains(t, contentType, "application/json") + assert.Contains(t, string(body), `"status":"ok"`) } // TestUsagePanel verifies that the usage panel shows memory and session stats.