diff --git a/tests/tracing_test.go b/tests/tracing_test.go
new file mode 100644
index 000000000..7ca80dba6
--- /dev/null
+++ b/tests/tracing_test.go
@@ -0,0 +1,229 @@
+package tests
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/dop251/goja"
+ "github.com/stretchr/testify/require"
+ "go.opentelemetry.io/otel/trace"
+
+ "github.com/grafana/xk6-browser/browser"
+ "github.com/grafana/xk6-browser/k6ext/k6test"
+ browsertrace "github.com/grafana/xk6-browser/trace"
+
+ k6lib "go.k6.io/k6/lib"
+)
+
+const html = `
+
+
+
+
+ Clickable link test
+
+
+
+
+
Click Counter
+
+ Type input
+
+
+
+
+
+
+`
+
+// TestTracing verifies that all methods instrumented to generate
+// traces behave correctly.
+func TestTracing(t *testing.T) {
+ t.Parallel()
+
+ // Init tracing mocks
+ tracer := &mockTracer{
+ spans: make(map[string]struct{}),
+ }
+ tp := &mockTracerProvider{
+ tracer: tracer,
+ }
+ // Start test server
+ ts := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, html)
+ },
+ ))
+
+ // Initialize VU and browser module
+ vu := k6test.NewVU(t, k6test.WithTracerProvider(tp))
+
+ rt := vu.Runtime()
+ root := browser.New()
+ mod := root.NewModuleInstance(vu)
+ jsMod, ok := mod.Exports().Default.(*browser.JSModule)
+ require.Truef(t, ok, "unexpected default mod export type %T", mod.Exports().Default)
+ require.NoError(t, rt.Set("browser", jsMod.Browser))
+ vu.ActivateVU()
+
+ // Run the test
+ vu.StartIteration(t)
+ require.NoError(t, tracer.verifySpans("iteration"))
+ setupTestTracing(t, rt)
+
+ testCases := []struct {
+ name string
+ js string
+ spans []string
+ }{
+ {
+ name: "browser.newPage",
+ js: "page = browser.newPage()",
+ spans: []string{
+ "browser.newPage",
+ "browser.newContext",
+ "browserContext.newPage",
+ },
+ },
+ {
+ name: "page.goto",
+ js: fmt.Sprintf("page.goto('%s')", ts.URL),
+ spans: []string{
+ "page.goto",
+ fmt.Sprintf("%s/", ts.URL), // Navigation span
+ },
+ },
+ {
+ name: "web_vital",
+ js: "sleep(100);", // Wait for async WebVitals processing
+ spans: []string{
+ "web_vital",
+ },
+ },
+ {
+ name: "page.screenshot",
+ js: "page.screenshot();",
+ spans: []string{
+ "page.screenshot",
+ },
+ },
+ {
+ name: "locator.click",
+ js: "page.locator('#clickme').click();",
+ spans: []string{
+ "locator.click",
+ },
+ },
+ {
+ name: "locator.type",
+ js: "page.locator('input#typeme').type('test');",
+ spans: []string{
+ "locator.type",
+ },
+ },
+ {
+ name: "page.reload",
+ js: `await Promise.all([
+ page.waitForNavigation(),
+ page.reload(),
+ ]);`,
+ spans: []string{
+ "page.reload",
+ "page.waitForNavigation",
+ },
+ },
+ {
+ name: "page.close",
+ js: "page.close()",
+ spans: []string{
+ "page.close",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ assertJSInEventLoop(t, vu, tc.js)
+ require.NoError(t, tracer.verifySpans(tc.spans...))
+ }
+}
+
+func setupTestTracing(t *testing.T, rt *goja.Runtime) {
+ t.Helper()
+
+ // Declare a global page var that we can use
+ // throughout the test cases
+ _, err := rt.RunString("var page;")
+ require.NoError(t, err)
+
+ // Set a sleep function so we can use it to wait
+ // for async WebVitals processing
+ err = rt.Set("sleep", func(d int) {
+ time.Sleep(time.Duration(d) * time.Millisecond)
+ })
+ require.NoError(t, err)
+}
+
+func assertJSInEventLoop(t *testing.T, vu *k6test.VU, js string) {
+ t.Helper()
+
+ f := fmt.Sprintf(
+ "test = async function() { %s; }",
+ js)
+
+ rt := vu.Runtime()
+ _, err := rt.RunString(f)
+ require.NoError(t, err)
+
+ test, ok := goja.AssertFunction(rt.Get("test"))
+ require.True(t, ok)
+
+ err = vu.Loop.Start(func() error {
+ _, err := test(goja.Undefined())
+ return err
+ })
+ require.NoError(t, err)
+}
+
+type mockTracerProvider struct {
+ k6lib.TracerProvider
+
+ tracer trace.Tracer
+}
+
+func (m *mockTracerProvider) Tracer(
+ name string, options ...trace.TracerOption, //nolint:revive
+) trace.Tracer {
+ return m.tracer
+}
+
+type mockTracer struct {
+ spans map[string]struct{}
+}
+
+func (m *mockTracer) Start(
+ ctx context.Context, spanName string, opts ...trace.SpanStartOption, //nolint:revive
+) (context.Context, trace.Span) {
+ m.spans[spanName] = struct{}{}
+ return ctx, browsertrace.NoopSpan{}
+}
+
+func (m *mockTracer) verifySpans(spanNames ...string) error {
+ for _, sn := range spanNames {
+ if _, ok := m.spans[sn]; !ok {
+ return fmt.Errorf("%q span was not found", sn)
+ }
+ delete(m.spans, sn)
+ }
+ return nil
+}