Skip to content

Commit d73b029

Browse files
authored
browser: implement page.on('requestfinished') event handler (#5486)
1 parent cc9f851 commit d73b029

6 files changed

Lines changed: 172 additions & 4 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { browser } from 'k6/browser'
2+
3+
export const options = {
4+
scenarios: {
5+
ui: {
6+
executor: 'shared-iterations',
7+
options: {
8+
browser: {
9+
type: 'chromium',
10+
},
11+
},
12+
},
13+
},
14+
}
15+
16+
export default async function () {
17+
const page = await browser.newPage()
18+
19+
// Track all completed requests
20+
const finishedRequests = []
21+
22+
page.on('requestfinished', request => {
23+
finishedRequests.push({
24+
url: request.url(),
25+
method: request.method(),
26+
resourceType: request.resourceType(),
27+
})
28+
29+
console.log(`✓ Request finished: ${request.method()} ${request.url()}`)
30+
})
31+
32+
await page.goto('https://quickpizza.grafana.com/', { waitUntil: 'networkidle' })
33+
34+
console.log(`Total requests completed: ${finishedRequests.length}`)
35+
36+
// Log all API requests
37+
const apiRequests = finishedRequests.filter(r => r.url.includes('/api/'))
38+
console.log(`API requests: ${apiRequests.length}`)
39+
40+
await page.close()
41+
}

internal/js/modules/k6/browser/browser/page_mapping.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -738,10 +738,11 @@ func mapPageOn(vu moduleVU, p *common.Page) func(common.PageEventName, sobek.Cal
738738
mapp func(vu moduleVU, event common.PageEvent) mapping
739739
wait bool // Whether to wait for the handler to complete.
740740
}{
741-
common.PageEventConsole: {mapp: mapConsoleMessage},
742-
common.PageEventMetric: {mapp: mapMetricEvent, wait: true},
743-
common.PageEventRequest: {mapp: mapRequestEvent},
744-
common.PageEventResponse: {mapp: mapResponseEvent},
741+
common.PageEventConsole: {mapp: mapConsoleMessage},
742+
common.PageEventMetric: {mapp: mapMetricEvent, wait: true},
743+
common.PageEventRequest: {mapp: mapRequestEvent},
744+
common.PageEventResponse: {mapp: mapResponseEvent},
745+
common.PageEventRequestFinished: {mapp: mapRequestEvent},
745746
}
746747
pageEvent, ok := pageEvents[eventName]
747748
if !ok {

internal/js/modules/k6/browser/common/network_manager.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type eventInterceptor interface {
4949
urlTagName(urlTag string, method string) (string, bool)
5050
onRequest(request *Request)
5151
onResponse(response *Response)
52+
onRequestFinished(request *Request)
5253
}
5354

5455
// NetworkManager manages all frames in HTML document.
@@ -436,6 +437,7 @@ func (m *NetworkManager) onLoadingFinished(event *network.EventLoadingFinished)
436437
req.responseEndTiming = float64(event.Timestamp.Time().Unix()-req.timestamp.Unix()) * 1000
437438
m.deleteRequestByID(event.RequestID)
438439
m.frameManager.requestFinished(req)
440+
m.eventInterceptor.onRequestFinished(req)
439441

440442
// Skip data and blob URLs when emitting metrics, since they're internal to the browser.
441443
if isInternalURL(req.url) {

internal/js/modules/k6/browser/common/network_manager_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ func (m *EventInterceptorMock) onRequest(_ *Request) {}
223223

224224
func (m *EventInterceptorMock) onResponse(_ *Response) {}
225225

226+
func (m *EventInterceptorMock) onRequestFinished(_ *Request) {}
227+
226228
func TestNetworkManagerEmitRequestResponseMetricsTimingSkew(t *testing.T) {
227229
t.Parallel()
228230

internal/js/modules/k6/browser/common/page.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const (
4848

4949
// PageEventResponse represents the page response event.
5050
PageEventResponse PageEventName = "response"
51+
52+
// PageEventRequestFinished represents the page request finished event.
53+
PageEventRequestFinished PageEventName = "requestfinished"
5154
)
5255

5356
// PageEventHandler is a function type that handles a page on event.
@@ -508,6 +511,16 @@ func (p *Page) onResponse(resp *Response) {
508511
}
509512
}
510513

514+
// onRequestFinished calls [PageEventRequestFinished] handlers when a request completes successfully.
515+
func (p *Page) onRequestFinished(request *Request) {
516+
for handle := range p.eventHandlersByName(PageEventRequestFinished) {
517+
if err := handle(PageEvent{Request: request}); err != nil {
518+
p.logger.Warnf("onRequestFinished", "handler returned an error: %v", err)
519+
return
520+
}
521+
}
522+
}
523+
511524
func (p *Page) onConsoleAPICalled(event *runtime.EventConsoleAPICalled) {
512525
if !p.hasEventHandler(PageEventConsole) {
513526
return

internal/js/modules/k6/browser/tests/page_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2809,6 +2809,115 @@ func TestPageOnResponse(t *testing.T) {
28092809
}
28102810
}
28112811

2812+
// TestPageOnRequestFinished tests that the requestfinished event fires when requests complete successfully.
2813+
func TestPageOnRequestFinished(t *testing.T) {
2814+
t.Parallel()
2815+
2816+
tb := newTestBrowser(t, withHTTPServer())
2817+
tb.withHandler("/home", func(w http.ResponseWriter, _ *http.Request) {
2818+
_, err := fmt.Fprintf(w, `<!DOCTYPE html>
2819+
<html>
2820+
<head>
2821+
<link rel="stylesheet" href="/style.css">
2822+
</head>
2823+
<body>
2824+
<script>fetch('/api', {
2825+
method: 'POST',
2826+
headers: {
2827+
'Content-Type': 'application/json'
2828+
},
2829+
body: JSON.stringify({name: 'tester'})
2830+
})</script>
2831+
</body>
2832+
</html>`)
2833+
require.NoError(t, err)
2834+
})
2835+
tb.withHandler("/api", func(w http.ResponseWriter, r *http.Request) {
2836+
body, err := io.ReadAll(r.Body)
2837+
require.NoError(t, err)
2838+
defer require.NoError(t, r.Body.Close())
2839+
2840+
var data struct {
2841+
Name string `json:"name"`
2842+
}
2843+
err = json.Unmarshal(body, &data)
2844+
require.NoError(t, err)
2845+
2846+
w.Header().Set("Content-Type", "application/json")
2847+
_, err = fmt.Fprintf(w, `{"message": "Hello %s!"}`, data.Name)
2848+
require.NoError(t, err)
2849+
})
2850+
tb.withHandler("/style.css", func(w http.ResponseWriter, _ *http.Request) {
2851+
w.Header().Set("Content-Type", "text/css")
2852+
_, err := fmt.Fprintf(w, `body { background-color: #f0f0f0; }`)
2853+
require.NoError(t, err)
2854+
})
2855+
2856+
tb.vu.ActivateVU()
2857+
tb.vu.StartIteration(t)
2858+
defer tb.vu.EndIteration(t)
2859+
2860+
gv, err := tb.vu.RunAsync(t, `
2861+
const context = await browser.newContext();
2862+
const page = await context.newPage();
2863+
2864+
var finishedRequests = [];
2865+
page.on('requestfinished', (request) => {
2866+
finishedRequests.push({
2867+
url: request.url(),
2868+
method: request.method(),
2869+
resourceType: request.resourceType(),
2870+
isNavigationRequest: request.isNavigationRequest(),
2871+
});
2872+
});
2873+
2874+
await page.goto('%s', {waitUntil: 'networkidle'});
2875+
await page.close();
2876+
return JSON.stringify(finishedRequests, null, 2);
2877+
`, tb.url("/home"))
2878+
require.NoError(t, err)
2879+
2880+
got := k6test.ToPromise(t, gv)
2881+
require.Equal(t, sobek.PromiseStateFulfilled, got.State())
2882+
2883+
var finishedRequests []struct {
2884+
URL string `json:"url"`
2885+
Method string `json:"method"`
2886+
ResourceType string `json:"resourceType"`
2887+
IsNavigationRequest bool `json:"isNavigationRequest"`
2888+
}
2889+
err = json.Unmarshal([]byte(got.Result().String()), &finishedRequests)
2890+
require.NoError(t, err)
2891+
2892+
// Verify we captured some finished requests
2893+
require.NotEmpty(t, finishedRequests, "expected to capture at least one finished request")
2894+
2895+
var foundHome, foundAPI, foundCSS bool
2896+
for _, req := range finishedRequests {
2897+
switch {
2898+
case strings.HasSuffix(req.URL, "/home"):
2899+
foundHome = true
2900+
assert.Equal(t, "GET", req.Method)
2901+
assert.Equal(t, "Document", req.ResourceType)
2902+
assert.True(t, req.IsNavigationRequest)
2903+
case strings.HasSuffix(req.URL, "/api"):
2904+
foundAPI = true
2905+
assert.Equal(t, "POST", req.Method)
2906+
assert.Equal(t, "Fetch", req.ResourceType)
2907+
assert.False(t, req.IsNavigationRequest)
2908+
case strings.HasSuffix(req.URL, "/style.css"):
2909+
foundCSS = true
2910+
assert.Equal(t, "GET", req.Method)
2911+
assert.Equal(t, "Stylesheet", req.ResourceType)
2912+
assert.False(t, req.IsNavigationRequest)
2913+
}
2914+
}
2915+
2916+
assert.True(t, foundHome, "expected to find /home request in finished requests")
2917+
assert.True(t, foundAPI, "expected to find /api request in finished requests")
2918+
assert.True(t, foundCSS, "expected to find /style.css request in finished requests")
2919+
}
2920+
28122921
func TestPageMustUseNativeJavaScriptObjects(t *testing.T) {
28132922
t.Parallel()
28142923

0 commit comments

Comments
 (0)