Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 73 additions & 11 deletions acceptance/dashboard_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package acceptance

import (
"fmt"
"net/url"
"testing"
"time"

Expand Down Expand Up @@ -332,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)
}

Expand Down Expand Up @@ -379,3 +376,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
}
60 changes: 60 additions & 0 deletions acceptance/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,66 @@ 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": "request-body-data"}`)
dashboard.WaitForEventCount(1, 5000)

// Click the event to see details
dashboard.ClickFirstEvent()
dashboard.WaitForEventDetails(5000)

// 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.
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 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 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.
func TestUsagePanel(t *testing.T) {
t.Parallel()
Expand Down
8 changes: 4 additions & 4 deletions dashboard/views/event-details.templ
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ templ HTTPRequestDetails(event *collector.Event, request collector.HTTPClientReq
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold">Body</h4>
<a
href={ templ.SafeURL(fmt.Sprintf("%s/download/request-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) }
href={ templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadRequestBodyURL(event.ID.String())) }
class="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
download
>
Expand Down Expand Up @@ -215,7 +215,7 @@ templ HTTPRequestDetails(event *collector.Event, request collector.HTTPClientReq
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold">Body</h4>
<a
href={ templ.SafeURL(fmt.Sprintf("%s/download/response-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) }
href={ templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadResponseBodyURL(event.ID.String())) }
class="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
download
>
Expand Down Expand Up @@ -314,7 +314,7 @@ templ HTTPServerRequestDetails(event *collector.Event, request collector.HTTPSer
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold">Body</h4>
<a
href={ templ.SafeURL(fmt.Sprintf("%s/download/request-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) }
href={ templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadRequestBodyURL(event.ID.String())) }
class="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
download
>
Expand Down Expand Up @@ -362,7 +362,7 @@ templ HTTPServerRequestDetails(event *collector.Event, request collector.HTTPSer
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold">Body</h4>
<a
href={ templ.SafeURL(fmt.Sprintf("%s/download/response-body/%s", MustGetHandlerOptions(ctx).PathPrefix, event.ID)) }
href={ templ.SafeURL(MustGetHandlerOptions(ctx).BuildDownloadResponseBodyURL(event.ID.String())) }
class="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1"
download
>
Expand Down
8 changes: 4 additions & 4 deletions dashboard/views/event-details_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions dashboard/views/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down