915 lines
25 KiB
Go
915 lines
25 KiB
Go
package playwright
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/playwright-community/playwright-go/internal/safe"
|
|
)
|
|
|
|
type browserContextImpl struct {
|
|
channelOwner
|
|
timeoutSettings *timeoutSettings
|
|
closeWasCalled bool
|
|
options *BrowserNewContextOptions
|
|
pages []Page
|
|
routes []*routeHandlerEntry
|
|
webSocketRoutes []*webSocketRouteHandler
|
|
ownedPage Page
|
|
browser *browserImpl
|
|
serviceWorkers []Worker
|
|
backgroundPages []Page
|
|
bindings *safe.SyncMap[string, BindingCallFunction]
|
|
tracing *tracingImpl
|
|
request *apiRequestContextImpl
|
|
harRecorders map[string]harRecordingMetadata
|
|
closed chan struct{}
|
|
closeReason *string
|
|
harRouters []*harRouter
|
|
clock Clock
|
|
}
|
|
|
|
func (b *browserContextImpl) Clock() Clock {
|
|
return b.clock
|
|
}
|
|
|
|
func (b *browserContextImpl) SetDefaultNavigationTimeout(timeout float64) {
|
|
b.setDefaultNavigationTimeoutImpl(&timeout)
|
|
}
|
|
|
|
func (b *browserContextImpl) setDefaultNavigationTimeoutImpl(timeout *float64) {
|
|
b.timeoutSettings.SetDefaultNavigationTimeout(timeout)
|
|
b.channel.SendNoReplyInternal("setDefaultNavigationTimeoutNoReply", map[string]interface{}{
|
|
"timeout": timeout,
|
|
})
|
|
}
|
|
|
|
func (b *browserContextImpl) SetDefaultTimeout(timeout float64) {
|
|
b.setDefaultTimeoutImpl(&timeout)
|
|
}
|
|
|
|
func (b *browserContextImpl) setDefaultTimeoutImpl(timeout *float64) {
|
|
b.timeoutSettings.SetDefaultTimeout(timeout)
|
|
b.channel.SendNoReplyInternal("setDefaultTimeoutNoReply", map[string]interface{}{
|
|
"timeout": timeout,
|
|
})
|
|
}
|
|
|
|
func (b *browserContextImpl) Pages() []Page {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
return b.pages
|
|
}
|
|
|
|
func (b *browserContextImpl) Browser() Browser {
|
|
return b.browser
|
|
}
|
|
|
|
func (b *browserContextImpl) Tracing() Tracing {
|
|
return b.tracing
|
|
}
|
|
|
|
func (b *browserContextImpl) NewCDPSession(page interface{}) (CDPSession, error) {
|
|
params := map[string]interface{}{}
|
|
|
|
if p, ok := page.(*pageImpl); ok {
|
|
params["page"] = p.channel
|
|
} else if f, ok := page.(*frameImpl); ok {
|
|
params["frame"] = f.channel
|
|
} else {
|
|
return nil, fmt.Errorf("not page or frame: %v", page)
|
|
}
|
|
|
|
channel, err := b.channel.Send("newCDPSession", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cdpSession := fromChannel(channel).(*cdpSessionImpl)
|
|
|
|
return cdpSession, nil
|
|
}
|
|
|
|
func (b *browserContextImpl) NewPage() (Page, error) {
|
|
if b.ownedPage != nil {
|
|
return nil, errors.New("Please use browser.NewContext()")
|
|
}
|
|
channel, err := b.channel.Send("newPage")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fromChannel(channel).(*pageImpl), nil
|
|
}
|
|
|
|
func (b *browserContextImpl) Cookies(urls ...string) ([]Cookie, error) {
|
|
result, err := b.channel.Send("cookies", map[string]interface{}{
|
|
"urls": urls,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cookies := make([]Cookie, len(result.([]interface{})))
|
|
for i, item := range result.([]interface{}) {
|
|
cookie := &Cookie{}
|
|
remapMapToStruct(item, cookie)
|
|
cookies[i] = *cookie
|
|
}
|
|
return cookies, nil
|
|
}
|
|
|
|
func (b *browserContextImpl) AddCookies(cookies []OptionalCookie) error {
|
|
_, err := b.channel.Send("addCookies", map[string]interface{}{
|
|
"cookies": cookies,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) ClearCookies(options ...BrowserContextClearCookiesOptions) error {
|
|
params := map[string]interface{}{}
|
|
if len(options) == 1 {
|
|
if options[0].Domain != nil {
|
|
switch t := options[0].Domain.(type) {
|
|
case string:
|
|
params["domain"] = t
|
|
case *string:
|
|
params["domain"] = t
|
|
case *regexp.Regexp:
|
|
pattern, flag := convertRegexp(t)
|
|
params["domainRegexSource"] = pattern
|
|
params["domainRegexFlags"] = flag
|
|
default:
|
|
return errors.New("invalid type for domain, expected string or *regexp.Regexp")
|
|
}
|
|
}
|
|
if options[0].Name != nil {
|
|
switch t := options[0].Name.(type) {
|
|
case string:
|
|
params["name"] = t
|
|
case *string:
|
|
params["name"] = t
|
|
case *regexp.Regexp:
|
|
pattern, flag := convertRegexp(t)
|
|
params["nameRegexSource"] = pattern
|
|
params["nameRegexFlags"] = flag
|
|
default:
|
|
return errors.New("invalid type for name, expected string or *regexp.Regexp")
|
|
}
|
|
}
|
|
if options[0].Path != nil {
|
|
switch t := options[0].Path.(type) {
|
|
case string:
|
|
params["path"] = t
|
|
case *string:
|
|
params["path"] = t
|
|
case *regexp.Regexp:
|
|
pattern, flag := convertRegexp(t)
|
|
params["pathRegexSource"] = pattern
|
|
params["pathRegexFlags"] = flag
|
|
default:
|
|
return errors.New("invalid type for path, expected string or *regexp.Regexp")
|
|
}
|
|
}
|
|
}
|
|
_, err := b.channel.Send("clearCookies", params)
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) GrantPermissions(permissions []string, options ...BrowserContextGrantPermissionsOptions) error {
|
|
_, err := b.channel.Send("grantPermissions", map[string]interface{}{
|
|
"permissions": permissions,
|
|
}, options)
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) ClearPermissions() error {
|
|
_, err := b.channel.Send("clearPermissions")
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) SetGeolocation(geolocation *Geolocation) error {
|
|
_, err := b.channel.Send("setGeolocation", map[string]interface{}{
|
|
"geolocation": geolocation,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) ResetGeolocation() error {
|
|
_, err := b.channel.Send("setGeolocation", map[string]interface{}{})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) SetExtraHTTPHeaders(headers map[string]string) error {
|
|
_, err := b.channel.Send("setExtraHTTPHeaders", map[string]interface{}{
|
|
"headers": serializeMapToNameAndValue(headers),
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) SetOffline(offline bool) error {
|
|
_, err := b.channel.Send("setOffline", map[string]interface{}{
|
|
"offline": offline,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) AddInitScript(script Script) error {
|
|
var source string
|
|
if script.Content != nil {
|
|
source = *script.Content
|
|
}
|
|
if script.Path != nil {
|
|
content, err := os.ReadFile(*script.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
source = string(content)
|
|
}
|
|
_, err := b.channel.Send("addInitScript", map[string]interface{}{
|
|
"source": source,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) ExposeBinding(name string, binding BindingCallFunction, handle ...bool) error {
|
|
needsHandle := false
|
|
if len(handle) == 1 {
|
|
needsHandle = handle[0]
|
|
}
|
|
for _, page := range b.Pages() {
|
|
if _, ok := page.(*pageImpl).bindings.Load(name); ok {
|
|
return fmt.Errorf("Function '%s' has been already registered in one of the pages", name)
|
|
}
|
|
}
|
|
if _, ok := b.bindings.Load(name); ok {
|
|
return fmt.Errorf("Function '%s' has been already registered", name)
|
|
}
|
|
_, err := b.channel.Send("exposeBinding", map[string]interface{}{
|
|
"name": name,
|
|
"needsHandle": needsHandle,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.bindings.Store(name, binding)
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) ExposeFunction(name string, binding ExposedFunction) error {
|
|
return b.ExposeBinding(name, func(source *BindingSource, args ...interface{}) interface{} {
|
|
return binding(args...)
|
|
})
|
|
}
|
|
|
|
func (b *browserContextImpl) Route(url interface{}, handler routeHandler, times ...int) error {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
b.routes = slices.Insert(b.routes, 0, newRouteHandlerEntry(newURLMatcher(url, b.options.BaseURL), handler, times...))
|
|
return b.updateInterceptionPatterns()
|
|
}
|
|
|
|
func (b *browserContextImpl) Unroute(url interface{}, handlers ...routeHandler) error {
|
|
removed, remaining, err := unroute(b.routes, url, handlers...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.unrouteInternal(removed, remaining, UnrouteBehaviorDefault)
|
|
}
|
|
|
|
func (b *browserContextImpl) unrouteInternal(removed []*routeHandlerEntry, remaining []*routeHandlerEntry, behavior *UnrouteBehavior) error {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
b.routes = remaining
|
|
if err := b.updateInterceptionPatterns(); err != nil {
|
|
return err
|
|
}
|
|
if behavior == nil || behavior == UnrouteBehaviorDefault {
|
|
return nil
|
|
}
|
|
wg := &sync.WaitGroup{}
|
|
for _, entry := range removed {
|
|
wg.Add(1)
|
|
go func(entry *routeHandlerEntry) {
|
|
defer wg.Done()
|
|
entry.Stop(string(*behavior))
|
|
}(entry)
|
|
}
|
|
wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
func (b *browserContextImpl) UnrouteAll(options ...BrowserContextUnrouteAllOptions) error {
|
|
var behavior *UnrouteBehavior
|
|
if len(options) == 1 {
|
|
behavior = options[0].Behavior
|
|
}
|
|
defer b.disposeHarRouters()
|
|
return b.unrouteInternal(b.routes, []*routeHandlerEntry{}, behavior)
|
|
}
|
|
|
|
func (b *browserContextImpl) disposeHarRouters() {
|
|
for _, router := range b.harRouters {
|
|
router.dispose()
|
|
}
|
|
b.harRouters = make([]*harRouter, 0)
|
|
}
|
|
|
|
func (b *browserContextImpl) Request() APIRequestContext {
|
|
return b.request
|
|
}
|
|
|
|
func (b *browserContextImpl) RouteFromHAR(har string, options ...BrowserContextRouteFromHAROptions) error {
|
|
opt := BrowserContextRouteFromHAROptions{}
|
|
if len(options) == 1 {
|
|
opt = options[0]
|
|
}
|
|
if opt.Update != nil && *opt.Update {
|
|
var updateContent *HarContentPolicy
|
|
switch opt.UpdateContent {
|
|
case RouteFromHarUpdateContentPolicyAttach:
|
|
updateContent = HarContentPolicyAttach
|
|
case RouteFromHarUpdateContentPolicyEmbed:
|
|
updateContent = HarContentPolicyEmbed
|
|
}
|
|
return b.recordIntoHar(har, browserContextRecordIntoHarOptions{
|
|
URL: opt.URL,
|
|
UpdateContent: updateContent,
|
|
UpdateMode: opt.UpdateMode,
|
|
})
|
|
}
|
|
notFound := opt.NotFound
|
|
if notFound == nil {
|
|
notFound = HarNotFoundAbort
|
|
}
|
|
router := newHarRouter(b.connection.localUtils, har, *notFound, opt.URL)
|
|
b.harRouters = append(b.harRouters, router)
|
|
return router.addContextRoute(b)
|
|
}
|
|
|
|
func (b *browserContextImpl) WaitForEvent(event string, options ...BrowserContextWaitForEventOptions) (interface{}, error) {
|
|
return b.waiterForEvent(event, options...).Wait()
|
|
}
|
|
|
|
func (b *browserContextImpl) waiterForEvent(event string, options ...BrowserContextWaitForEventOptions) *waiter {
|
|
timeout := b.timeoutSettings.Timeout()
|
|
var predicate interface{} = nil
|
|
if len(options) == 1 {
|
|
if options[0].Timeout != nil {
|
|
timeout = *options[0].Timeout
|
|
}
|
|
predicate = options[0].Predicate
|
|
}
|
|
waiter := newWaiter().WithTimeout(timeout)
|
|
waiter.RejectOnEvent(b, "close", ErrTargetClosed)
|
|
return waiter.WaitForEvent(b, event, predicate)
|
|
}
|
|
|
|
func (b *browserContextImpl) ExpectConsoleMessage(cb func() error, options ...BrowserContextExpectConsoleMessageOptions) (ConsoleMessage, error) {
|
|
var w *waiter
|
|
if len(options) == 1 {
|
|
w = b.waiterForEvent("console", BrowserContextWaitForEventOptions{
|
|
Predicate: options[0].Predicate,
|
|
Timeout: options[0].Timeout,
|
|
})
|
|
} else {
|
|
w = b.waiterForEvent("console")
|
|
}
|
|
ret, err := w.RunAndWait(cb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ret.(ConsoleMessage), nil
|
|
}
|
|
|
|
func (b *browserContextImpl) ExpectEvent(event string, cb func() error, options ...BrowserContextExpectEventOptions) (interface{}, error) {
|
|
if len(options) == 1 {
|
|
return b.waiterForEvent(event, BrowserContextWaitForEventOptions(options[0])).RunAndWait(cb)
|
|
}
|
|
return b.waiterForEvent(event).RunAndWait(cb)
|
|
}
|
|
|
|
func (b *browserContextImpl) ExpectPage(cb func() error, options ...BrowserContextExpectPageOptions) (Page, error) {
|
|
var w *waiter
|
|
if len(options) == 1 {
|
|
w = b.waiterForEvent("page", BrowserContextWaitForEventOptions{
|
|
Predicate: options[0].Predicate,
|
|
Timeout: options[0].Timeout,
|
|
})
|
|
} else {
|
|
w = b.waiterForEvent("page")
|
|
}
|
|
ret, err := w.RunAndWait(cb)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ret.(Page), nil
|
|
}
|
|
|
|
func (b *browserContextImpl) Close(options ...BrowserContextCloseOptions) error {
|
|
if b.closeWasCalled {
|
|
return nil
|
|
}
|
|
if len(options) == 1 {
|
|
b.closeReason = options[0].Reason
|
|
}
|
|
b.closeWasCalled = true
|
|
|
|
_, err := b.channel.connection.WrapAPICall(func() (interface{}, error) {
|
|
return nil, b.request.Dispose(APIRequestContextDisposeOptions{
|
|
Reason: b.closeReason,
|
|
})
|
|
}, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
innerClose := func() (interface{}, error) {
|
|
for harId, harMetaData := range b.harRecorders {
|
|
overrides := map[string]interface{}{}
|
|
if harId != "" {
|
|
overrides["harId"] = harId
|
|
}
|
|
response, err := b.channel.Send("harExport", overrides)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
artifact := fromChannel(response).(*artifactImpl)
|
|
// Server side will compress artifact if content is attach or if file is .zip.
|
|
needCompressed := strings.HasSuffix(strings.ToLower(harMetaData.Path), ".zip")
|
|
if !needCompressed && harMetaData.Content == HarContentPolicyAttach {
|
|
tmpPath := harMetaData.Path + ".tmp"
|
|
if err := artifact.SaveAs(tmpPath); err != nil {
|
|
return nil, err
|
|
}
|
|
err = b.connection.localUtils.HarUnzip(tmpPath, harMetaData.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := artifact.SaveAs(harMetaData.Path); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := artifact.Delete(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
_, err = b.channel.connection.WrapAPICall(innerClose, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = b.channel.Send("close", map[string]interface{}{
|
|
"reason": b.closeReason,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
<-b.closed
|
|
return err
|
|
}
|
|
|
|
type browserContextRecordIntoHarOptions struct {
|
|
Page Page
|
|
URL interface{}
|
|
UpdateContent *HarContentPolicy
|
|
UpdateMode *HarMode
|
|
}
|
|
|
|
func (b *browserContextImpl) recordIntoHar(har string, options ...browserContextRecordIntoHarOptions) error {
|
|
overrides := map[string]interface{}{}
|
|
harOptions := recordHarInputOptions{
|
|
Path: har,
|
|
Content: HarContentPolicyAttach,
|
|
Mode: HarModeMinimal,
|
|
}
|
|
if len(options) == 1 {
|
|
if options[0].UpdateContent != nil {
|
|
harOptions.Content = options[0].UpdateContent
|
|
}
|
|
if options[0].UpdateMode != nil {
|
|
harOptions.Mode = options[0].UpdateMode
|
|
}
|
|
harOptions.URL = options[0].URL
|
|
overrides["options"] = prepareRecordHarOptions(harOptions)
|
|
if options[0].Page != nil {
|
|
overrides["page"] = options[0].Page.(*pageImpl).channel
|
|
}
|
|
}
|
|
harId, err := b.channel.Send("harStart", overrides)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.harRecorders[harId.(string)] = harRecordingMetadata{
|
|
Path: har,
|
|
Content: harOptions.Content,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *browserContextImpl) StorageState(paths ...string) (*StorageState, error) {
|
|
result, err := b.channel.SendReturnAsDict("storageState")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(paths) == 1 {
|
|
file, err := os.Create(paths[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.NewEncoder(file).Encode(result); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
var storageState StorageState
|
|
remapMapToStruct(result, &storageState)
|
|
return &storageState, nil
|
|
}
|
|
|
|
func (b *browserContextImpl) onBinding(binding *bindingCallImpl) {
|
|
function, ok := b.bindings.Load(binding.initializer["name"].(string))
|
|
if !ok || function == nil {
|
|
return
|
|
}
|
|
go binding.Call(function)
|
|
}
|
|
|
|
func (b *browserContextImpl) onClose() {
|
|
if b.browser != nil {
|
|
contexts := make([]BrowserContext, 0)
|
|
b.browser.Lock()
|
|
for _, context := range b.browser.contexts {
|
|
if context != b {
|
|
contexts = append(contexts, context)
|
|
}
|
|
}
|
|
b.browser.contexts = contexts
|
|
b.browser.Unlock()
|
|
}
|
|
b.disposeHarRouters()
|
|
b.Emit("close", b)
|
|
}
|
|
|
|
func (b *browserContextImpl) onPage(page Page) {
|
|
b.Lock()
|
|
b.pages = append(b.pages, page)
|
|
b.Unlock()
|
|
b.Emit("page", page)
|
|
opener, _ := page.Opener()
|
|
if opener != nil && !opener.IsClosed() {
|
|
opener.Emit("popup", page)
|
|
}
|
|
}
|
|
|
|
func (b *browserContextImpl) onRoute(route *routeImpl) {
|
|
b.Lock()
|
|
route.context = b
|
|
page := route.Request().(*requestImpl).safePage()
|
|
routes := make([]*routeHandlerEntry, len(b.routes))
|
|
copy(routes, b.routes)
|
|
b.Unlock()
|
|
|
|
checkInterceptionIfNeeded := func() {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
if len(b.routes) == 0 {
|
|
_, err := b.connection.WrapAPICall(func() (interface{}, error) {
|
|
err := b.updateInterceptionPatterns()
|
|
return nil, err
|
|
}, true)
|
|
if err != nil {
|
|
logger.Error("could not update interception patterns", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
url := route.Request().URL()
|
|
for _, handlerEntry := range routes {
|
|
// If the page or the context was closed we stall all requests right away.
|
|
if (page != nil && page.closeWasCalled) || b.closeWasCalled {
|
|
return
|
|
}
|
|
if !handlerEntry.Matches(url) {
|
|
continue
|
|
}
|
|
if !slices.ContainsFunc(b.routes, func(entry *routeHandlerEntry) bool {
|
|
return entry == handlerEntry
|
|
}) {
|
|
continue
|
|
}
|
|
if handlerEntry.WillExceed() {
|
|
b.routes = slices.DeleteFunc(b.routes, func(rhe *routeHandlerEntry) bool {
|
|
return rhe == handlerEntry
|
|
})
|
|
}
|
|
handled := handlerEntry.Handle(route)
|
|
checkInterceptionIfNeeded()
|
|
yes := <-handled
|
|
if yes {
|
|
return
|
|
}
|
|
}
|
|
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
|
|
// the method will throw an error - silence it.
|
|
_ = route.internalContinue(true)
|
|
}
|
|
|
|
func (b *browserContextImpl) updateInterceptionPatterns() error {
|
|
patterns := prepareInterceptionPatterns(b.routes)
|
|
_, err := b.channel.Send("setNetworkInterceptionPatterns", map[string]interface{}{
|
|
"patterns": patterns,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) pause() <-chan error {
|
|
ret := make(chan error, 1)
|
|
go func() {
|
|
_, err := b.channel.Send("pause")
|
|
ret <- err
|
|
}()
|
|
return ret
|
|
}
|
|
|
|
func (b *browserContextImpl) onBackgroundPage(ev map[string]interface{}) {
|
|
b.Lock()
|
|
p := fromChannel(ev["page"]).(*pageImpl)
|
|
p.browserContext = b
|
|
b.backgroundPages = append(b.backgroundPages, p)
|
|
b.Unlock()
|
|
b.Emit("backgroundpage", p)
|
|
}
|
|
|
|
func (b *browserContextImpl) onServiceWorker(worker *workerImpl) {
|
|
worker.context = b
|
|
b.serviceWorkers = append(b.serviceWorkers, worker)
|
|
b.Emit("serviceworker", worker)
|
|
}
|
|
|
|
func (b *browserContextImpl) setOptions(options *BrowserNewContextOptions, tracesDir *string) {
|
|
if options == nil {
|
|
options = &BrowserNewContextOptions{}
|
|
}
|
|
b.options = options
|
|
if b.options != nil && b.options.RecordHarPath != nil {
|
|
b.harRecorders[""] = harRecordingMetadata{
|
|
Path: *b.options.RecordHarPath,
|
|
Content: b.options.RecordHarContent,
|
|
}
|
|
}
|
|
if tracesDir != nil {
|
|
b.tracing.tracesDir = *tracesDir
|
|
}
|
|
}
|
|
|
|
func (b *browserContextImpl) BackgroundPages() []Page {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
return b.backgroundPages
|
|
}
|
|
|
|
func (b *browserContextImpl) ServiceWorkers() []Worker {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
return b.serviceWorkers
|
|
}
|
|
|
|
func (b *browserContextImpl) OnBackgroundPage(fn func(Page)) {
|
|
b.On("backgroundpage", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnClose(fn func(BrowserContext)) {
|
|
b.On("close", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnConsole(fn func(ConsoleMessage)) {
|
|
b.On("console", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnDialog(fn func(Dialog)) {
|
|
b.On("dialog", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnPage(fn func(Page)) {
|
|
b.On("page", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnRequest(fn func(Request)) {
|
|
b.On("request", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnRequestFailed(fn func(Request)) {
|
|
b.On("requestfailed", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnRequestFinished(fn func(Request)) {
|
|
b.On("requestfinished", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnResponse(fn func(Response)) {
|
|
b.On("response", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) OnWebError(fn func(WebError)) {
|
|
b.On("weberror", fn)
|
|
}
|
|
|
|
func (b *browserContextImpl) RouteWebSocket(url interface{}, handler func(WebSocketRoute)) error {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
b.webSocketRoutes = slices.Insert(b.webSocketRoutes, 0, newWebSocketRouteHandler(newURLMatcher(url, b.options.BaseURL), handler))
|
|
|
|
return b.updateWebSocketInterceptionPatterns()
|
|
}
|
|
|
|
func (b *browserContextImpl) onWebSocketRoute(wr WebSocketRoute) {
|
|
b.Lock()
|
|
index := slices.IndexFunc(b.webSocketRoutes, func(r *webSocketRouteHandler) bool {
|
|
return r.Matches(wr.URL())
|
|
})
|
|
if index == -1 {
|
|
b.Unlock()
|
|
_, err := wr.ConnectToServer()
|
|
if err != nil {
|
|
logger.Error("could not connect to WebSocket server", "error", err)
|
|
}
|
|
return
|
|
}
|
|
handler := b.webSocketRoutes[index]
|
|
b.Unlock()
|
|
handler.Handle(wr)
|
|
}
|
|
|
|
func (b *browserContextImpl) updateWebSocketInterceptionPatterns() error {
|
|
patterns := prepareWebSocketRouteHandlerInterceptionPatterns(b.webSocketRoutes)
|
|
_, err := b.channel.Send("setWebSocketInterceptionPatterns", map[string]interface{}{
|
|
"patterns": patterns,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (b *browserContextImpl) effectiveCloseReason() *string {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
if b.closeReason != nil {
|
|
return b.closeReason
|
|
}
|
|
if b.browser != nil {
|
|
return b.browser.closeReason
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newBrowserContext(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *browserContextImpl {
|
|
bt := &browserContextImpl{
|
|
timeoutSettings: newTimeoutSettings(nil),
|
|
pages: make([]Page, 0),
|
|
backgroundPages: make([]Page, 0),
|
|
routes: make([]*routeHandlerEntry, 0),
|
|
bindings: safe.NewSyncMap[string, BindingCallFunction](),
|
|
harRecorders: make(map[string]harRecordingMetadata),
|
|
closed: make(chan struct{}, 1),
|
|
harRouters: make([]*harRouter, 0),
|
|
}
|
|
bt.createChannelOwner(bt, parent, objectType, guid, initializer)
|
|
if parent.objectType == "Browser" {
|
|
bt.browser = fromChannel(parent.channel).(*browserImpl)
|
|
bt.browser.contexts = append(bt.browser.contexts, bt)
|
|
}
|
|
bt.tracing = fromChannel(initializer["tracing"]).(*tracingImpl)
|
|
bt.request = fromChannel(initializer["requestContext"]).(*apiRequestContextImpl)
|
|
bt.clock = newClock(bt)
|
|
bt.channel.On("bindingCall", func(params map[string]interface{}) {
|
|
bt.onBinding(fromChannel(params["binding"]).(*bindingCallImpl))
|
|
})
|
|
|
|
bt.channel.On("close", bt.onClose)
|
|
bt.channel.On("page", func(payload map[string]interface{}) {
|
|
bt.onPage(fromChannel(payload["page"]).(*pageImpl))
|
|
})
|
|
bt.channel.On("route", func(params map[string]interface{}) {
|
|
bt.channel.CreateTask(func() {
|
|
bt.onRoute(fromChannel(params["route"]).(*routeImpl))
|
|
})
|
|
})
|
|
bt.channel.On("webSocketRoute", func(params map[string]interface{}) {
|
|
bt.channel.CreateTask(func() {
|
|
bt.onWebSocketRoute(fromChannel(params["webSocketRoute"]).(*webSocketRouteImpl))
|
|
})
|
|
})
|
|
bt.channel.On("backgroundPage", bt.onBackgroundPage)
|
|
bt.channel.On("serviceWorker", func(params map[string]interface{}) {
|
|
bt.onServiceWorker(fromChannel(params["worker"]).(*workerImpl))
|
|
})
|
|
bt.channel.On("console", func(ev map[string]interface{}) {
|
|
message := newConsoleMessage(ev)
|
|
bt.Emit("console", message)
|
|
if message.page != nil {
|
|
message.page.Emit("console", message)
|
|
}
|
|
})
|
|
bt.channel.On("dialog", func(params map[string]interface{}) {
|
|
dialog := fromChannel(params["dialog"]).(*dialogImpl)
|
|
go func() {
|
|
hasListeners := bt.Emit("dialog", dialog)
|
|
page := dialog.page
|
|
if page != nil {
|
|
if page.Emit("dialog", dialog) {
|
|
hasListeners = true
|
|
}
|
|
}
|
|
if !hasListeners {
|
|
// Although we do similar handling on the server side, we still need this logic
|
|
// on the client side due to a possible race condition between two async calls:
|
|
// a) removing "dialog" listener subscription (client->server)
|
|
// b) actual "dialog" event (server->client)
|
|
if dialog.Type() == "beforeunload" {
|
|
_ = dialog.Accept()
|
|
} else {
|
|
_ = dialog.Dismiss()
|
|
}
|
|
}
|
|
}()
|
|
})
|
|
bt.channel.On(
|
|
"pageError", func(ev map[string]interface{}) {
|
|
pwErr := &Error{}
|
|
remapMapToStruct(ev["error"].(map[string]interface{})["error"], pwErr)
|
|
err := parseError(*pwErr)
|
|
page := fromNullableChannel(ev["page"])
|
|
if page != nil {
|
|
bt.Emit("weberror", newWebError(page.(*pageImpl), err))
|
|
page.(*pageImpl).Emit("pageerror", err)
|
|
} else {
|
|
bt.Emit("weberror", newWebError(nil, err))
|
|
}
|
|
},
|
|
)
|
|
bt.channel.On("request", func(ev map[string]interface{}) {
|
|
request := fromChannel(ev["request"]).(*requestImpl)
|
|
page := fromNullableChannel(ev["page"])
|
|
bt.Emit("request", request)
|
|
if page != nil {
|
|
page.(*pageImpl).Emit("request", request)
|
|
}
|
|
})
|
|
bt.channel.On("requestFailed", func(ev map[string]interface{}) {
|
|
request := fromChannel(ev["request"]).(*requestImpl)
|
|
failureText := ev["failureText"]
|
|
if failureText != nil {
|
|
request.failureText = failureText.(string)
|
|
}
|
|
page := fromNullableChannel(ev["page"])
|
|
request.setResponseEndTiming(ev["responseEndTiming"].(float64))
|
|
bt.Emit("requestfailed", request)
|
|
if page != nil {
|
|
page.(*pageImpl).Emit("requestfailed", request)
|
|
}
|
|
})
|
|
|
|
bt.channel.On("requestFinished", func(ev map[string]interface{}) {
|
|
request := fromChannel(ev["request"]).(*requestImpl)
|
|
response := fromNullableChannel(ev["response"])
|
|
page := fromNullableChannel(ev["page"])
|
|
request.setResponseEndTiming(ev["responseEndTiming"].(float64))
|
|
bt.Emit("requestfinished", request)
|
|
if page != nil {
|
|
page.(*pageImpl).Emit("requestfinished", request)
|
|
}
|
|
if response != nil {
|
|
close(response.(*responseImpl).finished)
|
|
}
|
|
})
|
|
bt.channel.On("response", func(ev map[string]interface{}) {
|
|
response := fromChannel(ev["response"]).(*responseImpl)
|
|
page := fromNullableChannel(ev["page"])
|
|
bt.Emit("response", response)
|
|
if page != nil {
|
|
page.(*pageImpl).Emit("response", response)
|
|
}
|
|
})
|
|
bt.Once("close", func() {
|
|
bt.closed <- struct{}{}
|
|
})
|
|
bt.setEventSubscriptionMapping(map[string]string{
|
|
"console": "console",
|
|
"dialog": "dialog",
|
|
"request": "request",
|
|
"response": "response",
|
|
"requestfinished": "requestFinished",
|
|
"responsefailed": "responseFailed",
|
|
})
|
|
return bt
|
|
}
|