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 }