diff --git a/acceptance/dashboard_page.go b/acceptance/dashboard_page.go index 568f726..791b335 100644 --- a/acceptance/dashboard_page.go +++ b/acceptance/dashboard_page.go @@ -2,6 +2,7 @@ package acceptance import ( "fmt" + "net/url" "testing" "time" @@ -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) } @@ -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 +} diff --git a/acceptance/dashboard_test.go b/acceptance/dashboard_test.go index 0ca87cb..36dc075 100644 --- a/acceptance/dashboard_test.go +++ b/acceptance/dashboard_test.go @@ -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() 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