Skip to content

Commit 7cfff0d

Browse files
committed
chore: integration: add tests for browser scripts and metrics
1 parent 296c2d1 commit 7cfff0d

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed

integration/browser-script.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { browser } from 'k6/browser';
2+
import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js';
3+
4+
export const options = {
5+
scenarios: {
6+
ui: {
7+
executor: 'shared-iterations',
8+
options: {
9+
browser: {
10+
type: 'chromium',
11+
},
12+
},
13+
},
14+
},
15+
thresholds: {
16+
checks: ['rate==1.0'],
17+
},
18+
};
19+
20+
export default async function () {
21+
const context = await browser.newContext();
22+
const page = await context.newPage();
23+
24+
try {
25+
// e-commerce site as a torture test for metric generation.
26+
await page.goto('https://www.amazon.com');
27+
} finally {
28+
await page.close();
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package integration_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"testing"
12+
"time"
13+
14+
prometheus "github.com/prometheus/client_model/go"
15+
)
16+
17+
// runCrocochrome executes `docker run` for the crocochrome image, forwarding port 8080 to the host.
18+
// When the test finishes, the container is (hopefully) killed.
19+
func runCrocochrome(t *testing.T) {
20+
t.Helper()
21+
22+
const crocochromeImage = "ghcr.io/grafana/crocochrome:v0.5.0@sha256:8919c5ff1808d4cb4342c9ffa1311f673eec5ee2ec1e95c92730420a8d87087a"
23+
t.Logf("Starting crocochrome %s", crocochromeImage)
24+
dockerCmd := exec.Command("docker", "run", "--rm", "-i", "-p", "8080:8080", crocochromeImage)
25+
dockerCmd.Stderr = os.Stderr
26+
err := dockerCmd.Start()
27+
if err != nil {
28+
t.Fatalf("starting crocochrome container: %v", err)
29+
}
30+
31+
t.Cleanup(func() {
32+
_ = dockerCmd.Wait()
33+
})
34+
t.Cleanup(func() {
35+
if dockerCmd.Process == nil {
36+
return
37+
}
38+
39+
_ = dockerCmd.Process.Signal(os.Interrupt)
40+
})
41+
42+
// Wait until crocochrome is reachable.
43+
readinessCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
44+
defer cancel()
45+
for {
46+
req, err := http.NewRequestWithContext(readinessCtx, http.MethodGet, "http://localhost:8080/metrics", nil)
47+
if err != nil {
48+
t.Fatalf("building crocochrome health request: %v", err)
49+
}
50+
resp, err := http.DefaultClient.Do(req)
51+
if resp != nil {
52+
resp.Body.Close()
53+
}
54+
55+
if err == nil && resp.StatusCode == http.StatusOK {
56+
t.Logf("Crocochrome up and running")
57+
return
58+
}
59+
60+
if errors.Is(err, context.DeadlineExceeded) {
61+
t.Fatalf("Timeout starting crocochrome: %v", err)
62+
}
63+
64+
if ps := dockerCmd.ProcessState; ps != nil {
65+
t.Fatalf("Crocochrome exited with code %v", ps.ExitCode())
66+
}
67+
68+
t.Logf("Crocochrome not ready yet")
69+
time.Sleep(time.Second)
70+
}
71+
}
72+
73+
// runBrowserScript wraps runScript, creating a crocochrome session before running k6 and passing the right WS url to
74+
// it. The session is deleted when k6 returns.
75+
func runBrowserScript(t *testing.T, scriptFileName string, env []string) []*prometheus.MetricFamily {
76+
t.Helper()
77+
78+
endpoint := "http://localhost:8080"
79+
80+
session, err := createSession(endpoint)
81+
if err != nil {
82+
t.Fatalf("creating crocochrome session: %v", err)
83+
}
84+
85+
defer func() {
86+
err := deleteSession(endpoint, session.ID)
87+
if err != nil {
88+
t.Fatalf("deleting crocochrome session: %v", err)
89+
}
90+
}()
91+
92+
env = append(env, "K6_BROWSER_WS_URL="+session.ChromiumVersion.WebSocketDebuggerURL)
93+
return runScript(t, scriptFileName, env)
94+
}
95+
96+
type sessionInfo struct {
97+
ID string `json:"id"`
98+
ChromiumVersion struct {
99+
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
100+
} `json:"chromiumVersion"`
101+
}
102+
103+
// createSession uses the crocochrome API to start a browser session.
104+
func createSession(endpoint string) (*sessionInfo, error) {
105+
resp, err := http.Post(endpoint+"/sessions", "application/json", nil)
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
if resp.StatusCode != http.StatusOK {
111+
return nil, fmt.Errorf("got unexpected status %d", resp.StatusCode)
112+
}
113+
114+
session := sessionInfo{}
115+
err = json.NewDecoder(resp.Body).Decode(&session)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
return &session, nil
121+
}
122+
123+
// deleteSession calls the crocochrome API to delete a session.
124+
func deleteSession(endpoint, sessionID string) error {
125+
req, err := http.NewRequest(http.MethodDelete, endpoint+"/sessions/"+sessionID, nil)
126+
if err != nil {
127+
return err
128+
}
129+
130+
resp, err := http.DefaultClient.Do(req)
131+
if err != nil {
132+
return err
133+
}
134+
135+
if resp.StatusCode != http.StatusOK {
136+
return fmt.Errorf("got unexpected status %d", resp.StatusCode)
137+
}
138+
139+
return nil
140+
}

integration/integration_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,41 @@ func TestSMK6(t *testing.T) {
387387
})
388388
}
389389

390+
func TestSMK6Browser(t *testing.T) {
391+
t.Parallel()
392+
393+
runCrocochrome(t)
394+
395+
t.Run("default settings", func(t *testing.T) {
396+
// Do not run this one in parallel, as crocochrome only supports one concurrent script run.
397+
398+
mfs := runBrowserScript(t, "browser-script.js", nil) // Default allowlist.
399+
400+
t.Run("includes expected metrics", func(t *testing.T) {
401+
t.Parallel()
402+
403+
wanted := []string{
404+
"probe_browser_data_received",
405+
"probe_browser_data_sent",
406+
"probe_browser_http_req_duration",
407+
"probe_browser_http_req_failed",
408+
"probe_browser_web_vital_cls",
409+
"probe_browser_web_vital_fcp",
410+
"probe_browser_web_vital_lcp",
411+
"probe_browser_web_vital_ttfb",
412+
}
413+
for _, w := range wanted {
414+
if !slices.ContainsFunc(mfs, func(mf *prometheus.MetricFamily) bool {
415+
return *mf.Name == w
416+
}) {
417+
t.Log(mfs)
418+
t.Fatalf("Missing metric %q", w)
419+
}
420+
}
421+
})
422+
})
423+
}
424+
390425
func equals(expected float64) func(float64) bool {
391426
return func(v float64) bool {
392427
return v == expected

0 commit comments

Comments
 (0)