Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide both Sync and Async JS APIs #1373

Merged
merged 20 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/sync_e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: sync_E2E
on:
# Enable manually triggering this workflow via the API or web UI
workflow_dispatch:
push:
branches:
- main
pull_request:
schedule:
# At 06:00 AM UTC from Monday through Friday
- cron: '0 6 * * 1-5'

defaults:
run:
shell: bash

jobs:
test:
strategy:
matrix:
go: [stable, tip]
platform: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
if: matrix.go != 'tip' || matrix.platform != 'windows-latest'
uses: actions/checkout@v4
- name: Install Go
if: matrix.go != 'tip' || matrix.platform != 'windows-latest'
uses: actions/setup-go@v5
with:
go-version: 1.x
- name: Install Go tip
if: matrix.go == 'tip' && matrix.platform != 'windows-latest'
run: |
go install golang.org/dl/gotip@latest
gotip download
echo "GOROOT=$HOME/sdk/gotip" >> "$GITHUB_ENV"
echo "GOPATH=$HOME/go" >> "$GITHUB_ENV"
echo "$HOME/go/bin" >> "$GITHUB_PATH"
echo "$HOME/sdk/gotip/bin" >> "$GITHUB_PATH"
- name: Install xk6
if: matrix.go != 'tip' || matrix.platform != 'windows-latest'
run: go install go.k6.io/xk6/cmd/xk6@master
- name: Build extension
if: matrix.go != 'tip' || matrix.platform != 'windows-latest'
run: |
which go
go version

GOPRIVATE="go.k6.io/k6" xk6 build \
--output ./k6extension \
--with github.com/grafana/xk6-browser=.
./k6extension version
- name: Run E2E tests
if: matrix.go != 'tip' || matrix.platform != 'windows-latest'
run: |
set -x
if [ "$RUNNER_OS" == "Linux" ]; then
export K6_BROWSER_EXECUTABLE_PATH=/usr/bin/google-chrome
fi
export K6_BROWSER_HEADLESS=true
for f in sync-examples/*.js; do
if [ "$f" == "sync-examples/sync_hosts.js" ] && [ "$RUNNER_OS" == "Windows" ]; then
echo "skipping $f on Windows"
continue
fi
./k6extension run -q "$f"
done
- name: Check screenshot
if: matrix.go != 'tip' || matrix.platform != 'windows-latest'
# TODO: Do something more sophisticated?
run: test -s screenshot.png
22 changes: 21 additions & 1 deletion browser/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type (
tracesMetadata map[string]string
filePersister filePersister
testRunID string
isSync bool // remove later
}

// JSModule exposes the properties available to the JS script.
Expand Down Expand Up @@ -67,6 +68,17 @@ func New() *RootModule {
}
}

// NewSync returns a pointer to a new RootModule instance that maps the
// browser's business logic to the synchronous version of the module's
// JS API.
func NewSync() *RootModule {
return &RootModule{
PidRegistry: &pidRegistry{},
inancgumus marked this conversation as resolved.
Show resolved Hide resolved
initOnce: &sync.Once{},
isSync: true,
}
}

// NewModuleInstance implements the k6modules.Module interface to return
// a new instance for each VU.
func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance {
Expand All @@ -78,9 +90,17 @@ func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance {
m.initOnce.Do(func() {
m.initialize(vu)
})

// decide whether to map the browser module to the async JS API or
// the sync one.
mapper := mapBrowserToGoja
if m.isSync {
mapper = syncMapBrowserToGoja
}

return &ModuleInstance{
mod: &JSModule{
Browser: mapBrowserToGoja(moduleVU{
Browser: mapper(moduleVU{
VU: vu,
pidRegistry: m.PidRegistry,
browserRegistry: newBrowserRegistry(
Expand Down
132 changes: 132 additions & 0 deletions browser/sync_browser_context_mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package browser

import (
"fmt"
"reflect"

"github.com/dop251/goja"

"github.com/grafana/xk6-browser/common"
"github.com/grafana/xk6-browser/k6error"
"github.com/grafana/xk6-browser/k6ext"
)

// syncMapBrowserContext is like mapBrowserContext but returns synchronous functions.
func syncMapBrowserContext(vu moduleVU, bc *common.BrowserContext) mapping { //nolint:funlen,gocognit,cyclop
rt := vu.Runtime()
return mapping{
"addCookies": bc.AddCookies,
"addInitScript": func(script goja.Value) error {
if !gojaValueExists(script) {
return nil
}

source := ""
switch script.ExportType() {
case reflect.TypeOf(string("")):
source = script.String()
case reflect.TypeOf(goja.Object{}):
opts := script.ToObject(rt)
for _, k := range opts.Keys() {
if k == "content" {
source = opts.Get(k).String()
}
}
default:
_, isCallable := goja.AssertFunction(script)
if !isCallable {
source = fmt.Sprintf("(%s);", script.ToString().String())
} else {
source = fmt.Sprintf("(%s)(...args);", script.ToString().String())
}
}

return bc.AddInitScript(source) //nolint:wrapcheck
},
"browser": bc.Browser,
"clearCookies": bc.ClearCookies,
"clearPermissions": bc.ClearPermissions,
"close": bc.Close,
"cookies": bc.Cookies,
"grantPermissions": func(permissions []string, opts goja.Value) error {
pOpts := common.NewGrantPermissionsOptions()
pOpts.Parse(vu.Context(), opts)

return bc.GrantPermissions(permissions, pOpts) //nolint:wrapcheck
},
"setDefaultNavigationTimeout": bc.SetDefaultNavigationTimeout,
"setDefaultTimeout": bc.SetDefaultTimeout,
"setGeolocation": bc.SetGeolocation,
"setHTTPCredentials": bc.SetHTTPCredentials, //nolint:staticcheck
"setOffline": bc.SetOffline,
"waitForEvent": func(event string, optsOrPredicate goja.Value) (*goja.Promise, error) {
ctx := vu.Context()
popts := common.NewWaitForEventOptions(
bc.Timeout(),
)
if err := popts.Parse(ctx, optsOrPredicate); err != nil {
return nil, fmt.Errorf("parsing waitForEvent options: %w", err)
}

return k6ext.Promise(ctx, func() (result any, reason error) {
var runInTaskQueue func(p *common.Page) (bool, error)
if popts.PredicateFn != nil {
runInTaskQueue = func(p *common.Page) (bool, error) {
tq := vu.taskQueueRegistry.get(p.TargetID())

var rtn bool
var err error
// The function on the taskqueue runs in its own goroutine
// so we need to use a channel to wait for it to complete
// before returning the result to the caller.
c := make(chan bool)
tq.Queue(func() error {
var resp goja.Value
resp, err = popts.PredicateFn(vu.Runtime().ToValue(p))
rtn = resp.ToBoolean()
close(c)
return nil
})
<-c

return rtn, err //nolint:wrapcheck
}
}

resp, err := bc.WaitForEvent(event, runInTaskQueue, popts.Timeout)
panicIfFatalError(ctx, err)
if err != nil {
return nil, err //nolint:wrapcheck
}
p, ok := resp.(*common.Page)
if !ok {
panicIfFatalError(ctx, fmt.Errorf("response object is not a page: %w", k6error.ErrFatal))
}

return syncMapPage(vu, p), nil
}), nil
},
"pages": func() *goja.Object {
var (
mpages []mapping
pages = bc.Pages()
)
for _, page := range pages {
if page == nil {
continue
}
m := syncMapPage(vu, page)
mpages = append(mpages, m)
}

return rt.ToValue(mpages).ToObject(rt)
},
"newPage": func() (mapping, error) {
page, err := bc.NewPage()
if err != nil {
return nil, err //nolint:wrapcheck
}
return syncMapPage(vu, page), nil
},
}
}
81 changes: 81 additions & 0 deletions browser/sync_browser_mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package browser

import (
"github.com/dop251/goja"
)

// syncMapBrowser is like mapBrowser but returns synchronous functions.
func syncMapBrowser(vu moduleVU) mapping { //nolint:funlen,cyclop
rt := vu.Runtime()
return mapping{
"context": func() (mapping, error) {
b, err := vu.browser()
if err != nil {
return nil, err
}
return syncMapBrowserContext(vu, b.Context()), nil
},
"closeContext": func() error {
b, err := vu.browser()
if err != nil {
return err
}
return b.CloseContext() //nolint:wrapcheck
},
"isConnected": func() (bool, error) {
b, err := vu.browser()
if err != nil {
return false, err
}
return b.IsConnected(), nil
},
"newContext": func(opts goja.Value) (*goja.Object, error) {
b, err := vu.browser()
if err != nil {
return nil, err
}
bctx, err := b.NewContext(opts)
if err != nil {
return nil, err //nolint:wrapcheck
}

if err := initBrowserContext(bctx, vu.testRunID); err != nil {
return nil, err
}

m := syncMapBrowserContext(vu, bctx)

return rt.ToValue(m).ToObject(rt), nil
},
"userAgent": func() (string, error) {
b, err := vu.browser()
if err != nil {
return "", err
}
return b.UserAgent(), nil
},
"version": func() (string, error) {
b, err := vu.browser()
if err != nil {
return "", err
}
return b.Version(), nil
},
"newPage": func(opts goja.Value) (mapping, error) {
b, err := vu.browser()
if err != nil {
return nil, err
}
page, err := b.NewPage(opts)
if err != nil {
return nil, err //nolint:wrapcheck
}

if err := initBrowserContext(b.Context(), vu.testRunID); err != nil {
return nil, err
}

return syncMapPage(vu, page), nil
},
}
}
31 changes: 31 additions & 0 deletions browser/sync_console_message_mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package browser

import (
"github.com/dop251/goja"

"github.com/grafana/xk6-browser/common"
)

// syncMapConsoleMessage is like mapConsoleMessage but returns synchronous functions.
func syncMapConsoleMessage(vu moduleVU, cm *common.ConsoleMessage) mapping {
rt := vu.Runtime()
return mapping{
"args": func() *goja.Object {
var (
margs []mapping
args = cm.Args
)
for _, arg := range args {
a := syncMapJSHandle(vu, arg)
margs = append(margs, a)
}

return rt.ToValue(margs).ToObject(rt)
},
// page(), text() and type() are defined as
// functions in order to match Playwright's API
"page": func() mapping { return syncMapPage(vu, cm.Page) },
"text": func() string { return cm.Text },
"type": func() string { return cm.Type },
}
}
Loading
Loading