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 +}