Skip to content

Commit

Permalink
Add mappings from the browser module to Goja
Browse files Browse the repository at this point in the history
As discussed in issue #661, we're now mapping the browser module methods
to Goja. The global Goja field name mappers are removed from the code.

The current mappers are just forwarders to the inner methods, and we add
$ and $$ for Query and QueryAll methods.

Let me know if I missed something while mapping the methods.
  • Loading branch information
inancgumus committed Dec 13, 2022
1 parent ae5c339 commit 4fa0f6b
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 22 deletions.
296 changes: 288 additions & 8 deletions browser/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package browser

import (
"errors"
"fmt"
"os"

"github.com/dop251/goja"

"github.com/grafana/xk6-browser/api"
"github.com/grafana/xk6-browser/chromium"
"github.com/grafana/xk6-browser/common"
Expand All @@ -22,7 +25,7 @@ type (

// JSModule exposes the properties available to the JS script.
JSModule struct {
Chromium api.BrowserType
Chromium *goja.Object
Devices map[string]common.Device
Version string
}
Expand All @@ -45,7 +48,7 @@ func New() *RootModule {

// NewModuleInstance implements the k6modules.Module interface to return
// a new instance for each VU.
func (*RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance {
func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance {
if _, ok := os.LookupEnv("K6_BROWSER_DISABLE_RUN"); ok {
msg := "Disable run flag enabled, browser test run aborted. Please contact support."
if m, ok := os.LookupEnv("K6_BROWSER_DISABLE_RUN_MSG"); ok {
Expand All @@ -54,13 +57,17 @@ func (*RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance {

k6common.Throw(vu.Runtime(), errors.New(msg))
}

return &ModuleInstance{
mod: &JSModule{
Chromium: chromium.NewBrowserType(vu),
Devices: common.GetDevices(),
Version: version,
},
mod: m.NewJSModule(vu),
}
}

// NewJSModule returns a new JSModule instance.
func (*RootModule) NewJSModule(vu k6modules.VU) *JSModule {
return &JSModule{
Chromium: mapBrowserToGoja(vu),
Devices: common.GetDevices(),
Version: version,
}
}

Expand All @@ -69,3 +76,276 @@ func (*RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance {
func (mi *ModuleInstance) Exports() k6modules.Exports {
return k6modules.Exports{Default: mi.mod}
}

type mapping map[string]any

// mapBrowserToGoja maps the browser API to the JS module.
// The motivation of this mapping was to support $ and $$ wildcard
// methods.
// See issue #661 for more details.
func mapBrowserToGoja(vu k6modules.VU) *goja.Object {
rt := vu.Runtime()

var (
obj = rt.NewObject()
browserType = chromium.NewBrowserType(vu)
)
for k, v := range mapBrowserType(rt, browserType) {
err := obj.Set(k, rt.ToValue(v))
if err != nil {
k6common.Throw(rt, fmt.Errorf("mapping: %w", err))
}
}

return obj
}

//nolint:funlen
func mapPage(rt *goja.Runtime, p api.Page) mapping {
maps := mapping{
"addInitScript": p.AddInitScript,
"addScriptTag": p.AddScriptTag,
"addStyleTag": p.AddStyleTag,
"bringToFront": p.BringToFront,
"check": p.Check,
"click": p.Click,
"close": p.Close,
"content": p.Content,
"context": p.Context,
"dblclick": p.Dblclick,
"dispatchEvent": p.DispatchEvent,
"dragAndDrop": p.DragAndDrop,
"emulateMedia": p.EmulateMedia,
"emulateVisionDeficiency": p.EmulateVisionDeficiency,
"evaluate": p.Evaluate,
"evaluateHandle": p.EvaluateHandle,
"exposeBinding": p.ExposeBinding,
"exposeFunction": p.ExposeFunction,
"fill": p.Fill,
"focus": p.Focus,
"frame": p.Frame,
"frames": p.Frames,
"getAttribute": p.GetAttribute,
"goBack": p.GoBack,
"goForward": p.GoForward,
"goto": p.Goto,
"hover": p.Hover,
"innerHTML": p.InnerHTML,
"innerText": p.InnerText,
"inputValue": p.InputValue,
"isChecked": p.IsChecked,
"isClosed": p.IsClosed,
"isDisabled": p.IsDisabled,
"isEditable": p.IsEditable,
"isEnabled": p.IsEnabled,
"isHidden": p.IsHidden,
"isVisible": p.IsVisible,
"locator": p.Locator,
"mainFrame": p.MainFrame,
"opener": p.Opener,
"pause": p.Pause,
"pdf": p.Pdf,
"press": p.Press,
"reload": p.Reload,
"route": p.Route,
"screenshot": p.Screenshot,
"selectOption": p.SelectOption,
"setContent": p.SetContent,
"setDefaultNavigationTimeout": p.SetDefaultNavigationTimeout,
"setDefaultTimeout": p.SetDefaultTimeout,
"setExtraHTTPHeaders": p.SetExtraHTTPHeaders,
"setInputFiles": p.SetInputFiles,
"setViewportSize": p.SetViewportSize,
"tap": p.Tap,
"textContent": p.TextContent,
"title": p.Title,
"type": p.Type,
"uncheck": p.Uncheck,
"unroute": p.Unroute,
"uRL": p.URL,
"video": p.Video,
"viewportSize": p.ViewportSize,
"waitForEvent": p.WaitForEvent,
"waitForFunction": p.WaitForFunction,
"waitForLoadState": p.WaitForLoadState,
"waitForNavigation": p.WaitForNavigation,
"waitForRequest": p.WaitForRequest,
"waitForResponse": p.WaitForResponse,
"waitForSelector": p.WaitForSelector,
"waitForTimeout": p.WaitForTimeout,
"workers": p.Workers,
"query": func(selector string) *goja.Object {
eh := p.Query(selector)
ehm := mapElementHandle(rt, eh)
return rt.ToValue(ehm).ToObject(rt)
},
"queryAll": func(selector string) *goja.Object {
var (
mehs []mapping
ehs = p.QueryAll(selector)
)
for _, eh := range ehs {
ehm := mapElementHandle(rt, eh)
mehs = append(mehs, ehm)
}
return rt.ToValue(mehs).ToObject(rt)
},
}
maps["$"] = maps["query"]
maps["$$"] = maps["queryAll"]

return maps
}

//nolint:funlen
func mapElementHandle(rt *goja.Runtime, eh api.ElementHandle) mapping {
maps := mapping{
"asElement": func() *goja.Object {
m := mapElementHandle(rt, eh.AsElement())
return rt.ToValue(m).ToObject(rt)
},
"dispose": eh.Dispose,
"evaluate": eh.Evaluate,
"evaluateHandle": eh.EvaluateHandle,
"getProperties": eh.GetProperties,
"getProperty": eh.GetProperty,
"jSONValue": eh.JSONValue,
"objectID": eh.ObjectID,
"boundingBox": eh.BoundingBox,
"check": eh.Check,
"click": eh.Click,
"contentFrame": eh.ContentFrame,
"dblclick": eh.Dblclick,
"dispatchEvent": eh.DispatchEvent,
"fill": eh.Fill,
"focus": eh.Focus,
"getAttribute": eh.GetAttribute,
"hover": eh.Hover,
"innerHTML": eh.InnerHTML,
"innerText": eh.InnerText,
"inputValue": eh.InputValue,
"isChecked": eh.IsChecked,
"isDisabled": eh.IsDisabled,
"isEditable": eh.IsEditable,
"isEnabled": eh.IsEnabled,
"isHidden": eh.IsHidden,
"isVisible": eh.IsVisible,
"ownerFrame": eh.OwnerFrame,
"press": eh.Press,
"screenshot": eh.Screenshot,
"scrollIntoViewIfNeeded": eh.ScrollIntoViewIfNeeded,
"selectOption": eh.SelectOption,
"selectText": eh.SelectText,
"setInputFiles": eh.SetInputFiles,
"tap": eh.Tap,
"textContent": eh.TextContent,
"type": eh.Type,
"uncheck": eh.Uncheck,
"waitForElementState": eh.WaitForElementState,
"waitForSelector": func(selector string, opts goja.Value) *goja.Object {
eh := eh.WaitForSelector(selector, opts)
ehm := mapElementHandle(rt, eh)
return rt.ToValue(ehm).ToObject(rt)
},
"query": func(selector string) *goja.Object {
eh := eh.Query(selector)
ehm := mapElementHandle(rt, eh)
return rt.ToValue(ehm).ToObject(rt)
},
"queryAll": func(selector string) *goja.Object {
var (
mehs []mapping
ehs = eh.QueryAll(selector)
)
for _, eh := range ehs {
ehm := mapElementHandle(rt, eh)
mehs = append(mehs, ehm)
}
return rt.ToValue(mehs).ToObject(rt)
},
}
maps["$"] = maps["query"]
maps["$$"] = maps["queryAll"]

return maps
}

func mapBrowserContext(rt *goja.Runtime, bc api.BrowserContext) mapping {
return mapping{
"addCookies": bc.AddCookies,
"addInitScript": bc.AddInitScript,
"browser": bc.Browser,
"clearCookies": bc.ClearCookies,
"clearPermissions": bc.ClearPermissions,
"close": bc.Close,
"cookies": bc.Cookies,
"exposeBinding": bc.ExposeBinding,
"exposeFunction": bc.ExposeFunction,
"grantPermissions": bc.GrantPermissions,
"newCDPSession": bc.NewCDPSession,
"route": bc.Route,
"setDefaultNavigationTimeout": bc.SetDefaultNavigationTimeout,
"setDefaultTimeout": bc.SetDefaultTimeout,
"setExtraHTTPHeaders": bc.SetExtraHTTPHeaders,
"setGeolocation": bc.SetGeolocation,
"setHTTPCredentials": bc.SetHTTPCredentials, //nolint:staticcheck
"setOffline": bc.SetOffline,
"storageState": bc.StorageState,
"unroute": bc.Unroute,
"waitForEvent": bc.WaitForEvent,
"pages": func() *goja.Object {
var (
mpages []mapping
pages = bc.Pages()
)
for _, page := range pages {
if page == nil {
continue
}
m := mapPage(rt, page)
mpages = append(mpages, m)
}

return rt.ToValue(mpages).ToObject(rt)
},
"newPage": func() *goja.Object {
page := bc.NewPage()
m := mapPage(rt, page)
return rt.ToValue(m).ToObject(rt)
},
}
}

func mapBrowser(rt *goja.Runtime, b api.Browser) mapping {
return mapping{
"close": b.Close,
"contexts": b.Contexts,
"isConnected": b.IsConnected,
"on": b.On,
"userAgent": b.UserAgent,
"version": b.Version,
"newContext": func(opts goja.Value) *goja.Object {
bctx := b.NewContext(opts)
m := mapBrowserContext(rt, bctx)
return rt.ToValue(m).ToObject(rt)
},
"newPage": func(opts goja.Value) *goja.Object {
page := b.NewPage(opts)
m := mapPage(rt, page)
return rt.ToValue(m).ToObject(rt)
},
}
}

func mapBrowserType(rt *goja.Runtime, bt api.BrowserType) mapping {
return mapping{
"connect": bt.Connect,
"executablePath": bt.ExecutablePath,
"launchPersistentContext": bt.LaunchPersistentContext,
"name": bt.Name,
"launch": func(opts goja.Value) *goja.Object {
m := mapBrowser(rt, bt.Launch(opts))
return rt.ToValue(m).ToObject(rt)
},
}
}
4 changes: 1 addition & 3 deletions browser/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/grafana/xk6-browser/chromium"

k6common "go.k6.io/k6/js/common"
k6modulestest "go.k6.io/k6/js/modulestest"
k6metrics "go.k6.io/k6/metrics"
Expand All @@ -29,7 +27,7 @@ func TestModuleNew(t *testing.T) {
m, ok := New().NewModuleInstance(vu).(*ModuleInstance)
require.True(t, ok, "NewModuleInstance should return a ModuleInstance")
require.NotNil(t, m.mod, "Module should be set")
require.IsType(t, m.mod.Chromium, &chromium.BrowserType{})
require.IsType(t, m.mod.Chromium, (*goja.Object)(nil), "Chromium should be a goja.Value")
require.NotNil(t, m.mod.Devices, "Devices should be set")
require.Equal(t, m.mod.Version, version, "Incorrect version")
}
Expand Down
8 changes: 1 addition & 7 deletions chromium/browser_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,16 @@ type BrowserType struct {
// NewBrowserType registers our custom k6 metrics, creates method mappings on
// the goja runtime, and returns a new Chrome browser type.
func NewBrowserType(vu k6modules.VU) api.BrowserType {
var (
rt = vu.Runtime()
hooks = common.NewHooks()
)

// NOTE: vu.InitEnv() *must* be called from the script init scope,
// otherwise it will return nil.
k6m := k6ext.RegisterCustomMetrics(vu.InitEnv().Registry)
b := BrowserType{
vu: vu,
hooks: hooks,
hooks: common.NewHooks(),
k6Metrics: k6m,
storage: &storage.Dir{},
randSrc: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint: gosec
}
rt.SetFieldNameMapper(common.NewFieldNameMapper())

return &b
}
Expand Down
4 changes: 0 additions & 4 deletions k6ext/k6test/vu.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/grafana/xk6-browser/k6ext"

k6common "go.k6.io/k6/js/common"
k6eventloop "go.k6.io/k6/js/eventloop"
k6modulestest "go.k6.io/k6/js/modulestest"
k6lib "go.k6.io/k6/lib"
Expand Down Expand Up @@ -57,9 +56,6 @@ func (v *VU) AssertSamples(assertSample func(s k6metrics.Sample)) int {
func NewVU(tb testing.TB) *VU {
tb.Helper()

rt := goja.New()
rt.SetFieldNameMapper(k6common.FieldNameMapper{})

samples := make(chan k6metrics.SampleContainer, 1000)

root, err := k6lib.NewGroup("", nil)
Expand Down

0 comments on commit 4fa0f6b

Please sign in to comment.