package playwright import ( "errors" "fmt" "path" "reflect" "regexp" "strings" "sync" "sync/atomic" mapset "github.com/deckarep/golang-set/v2" ) type ( routeHandler = func(Route) ) func skipFieldSerialization(val reflect.Value) bool { typ := val.Type() return (typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Interface || typ.Kind() == reflect.Map || typ.Kind() == reflect.Slice) && val.IsNil() || (val.Kind() == reflect.Interface && val.Elem().Kind() == reflect.Ptr && val.Elem().IsNil()) } func transformStructValues(in interface{}) interface{} { v := reflect.ValueOf(in) if v.Kind() == reflect.Ptr { v = v.Elem() } if _, ok := in.(*channel); ok { return in } if v.Kind() == reflect.Map || v.Kind() == reflect.Struct { return transformStructIntoMapIfNeeded(in) } if v.Kind() == reflect.Slice { outSlice := []interface{}{} for i := 0; i < v.Len(); i++ { if !skipFieldSerialization(v.Index(i)) { outSlice = append(outSlice, transformStructValues(v.Index(i).Interface())) } } return outSlice } if v.Interface() == Null() || (v.Kind() == reflect.String && v.String() == Null().(string)) { return "null" } return in } func transformStructIntoMapIfNeeded(inStruct interface{}) map[string]interface{} { out := make(map[string]interface{}) v := reflect.ValueOf(inStruct) if v.Kind() == reflect.Ptr { v = v.Elem() } typ := v.Type() if v.Kind() == reflect.Struct { // Merge into the base map by the JSON struct tag for i := 0; i < v.NumField(); i++ { fi := typ.Field(i) // Skip the values when the field is a pointer (like *string) and nil. if fi.IsExported() && !skipFieldSerialization(v.Field(i)) { // We use the JSON struct fields for getting the original names // out of the field. tagv := fi.Tag.Get("json") key := strings.Split(tagv, ",")[0] if key == "" { key = fi.Name } out[key] = transformStructValues(v.Field(i).Interface()) } } } else if v.Kind() == reflect.Map { // Merge into the base map for _, key := range v.MapKeys() { if !skipFieldSerialization(v.MapIndex(key)) { out[key.String()] = transformStructValues(v.MapIndex(key).Interface()) } } } return out } // transformOptions handles the parameter data transformation func transformOptions(options ...interface{}) map[string]interface{} { var base map[string]interface{} var option interface{} // Case 1: No options are given if len(options) == 0 { return make(map[string]interface{}) } if len(options) == 1 { // Case 2: a single value (either struct or map) is given. base = make(map[string]interface{}) option = options[0] } else if len(options) == 2 { // Case 3: two values are given. The first one needs to be transformed // to a map, the sencond one will be then get merged into the first // base map. if reflect.ValueOf(options[0]).Kind() != reflect.Map { base = transformOptions(options[0]) } else { base = transformStructIntoMapIfNeeded(options[0]) } option = options[1] } v := reflect.ValueOf(option) if v.Kind() == reflect.Slice { if v.Len() == 0 { return base } option = v.Index(0).Interface() } if option == nil { return base } v = reflect.ValueOf(option) if v.Kind() == reflect.Ptr { v = v.Elem() } optionMap := transformStructIntoMapIfNeeded(v.Interface()) for key, value := range optionMap { base[key] = value } return base } func remapValue(inMapValue reflect.Value, outStructValue reflect.Value) { switch outStructValue.Type().Kind() { case reflect.Bool: outStructValue.SetBool(inMapValue.Bool()) case reflect.String: outStructValue.SetString(inMapValue.String()) case reflect.Float64: outStructValue.SetFloat(inMapValue.Float()) case reflect.Int: outStructValue.SetInt(int64(inMapValue.Float())) case reflect.Slice: outStructValue.Set(reflect.MakeSlice(outStructValue.Type(), inMapValue.Len(), inMapValue.Cap())) for i := 0; i < inMapValue.Len(); i++ { remapValue(inMapValue.Index(i).Elem(), outStructValue.Index(i)) } case reflect.Struct: structTyp := outStructValue.Type() for i := 0; i < outStructValue.NumField(); i++ { fi := structTyp.Field(i) key := strings.Split(fi.Tag.Get("json"), ",")[0] structField := outStructValue.Field(i) structFieldDeref := outStructValue.Field(i) if structField.Type().Kind() == reflect.Ptr { structField.Set(reflect.New(structField.Type().Elem())) structFieldDeref = structField.Elem() } for _, e := range inMapValue.MapKeys() { if key == e.String() { value := inMapValue.MapIndex(e) remapValue(value.Elem(), structFieldDeref) } } } default: panic(inMapValue.Interface()) } } func remapMapToStruct(inputMap interface{}, outStruct interface{}) { remapValue(reflect.ValueOf(inputMap), reflect.ValueOf(outStruct).Elem()) } type urlMatcher struct { raw interface{} pattern *regexp.Regexp matchFn func(url string) bool } func newURLMatcher(urlOrPredicate, baseURL interface{}) *urlMatcher { switch v := urlOrPredicate.(type) { case *regexp.Regexp: return &urlMatcher{pattern: v, raw: urlOrPredicate} case string: url := v if baseURL != nil && !strings.HasPrefix(url, "*") { base, ok := baseURL.(*string) if ok && base != nil { url = path.Join(*base, url) } } return &urlMatcher{pattern: globMustToRegex(url), raw: urlOrPredicate} } fn, ok := urlOrPredicate.(func(string) bool) if ok { return &urlMatcher{ matchFn: fn, raw: urlOrPredicate, } } panic(fmt.Errorf("invalid urlOrPredicate: %v", urlOrPredicate)) } func (u *urlMatcher) Matches(url string) bool { if u.matchFn != nil { return u.matchFn(url) } if u.pattern != nil { return u.pattern.MatchString(url) } return false } // SameWith compares String() if urlOrPredicate is *regexp.Regexp func (u *urlMatcher) SameWith(urlOrPredicate interface{}) bool { switch v := urlOrPredicate.(type) { case *regexp.Regexp: return u.pattern.String() == v.String() default: return u.raw == urlOrPredicate } } type routeHandlerInvocation struct { route Route complete chan bool } type routeHandlerEntry struct { matcher *urlMatcher handler routeHandler times int count int32 ignoreErrors *atomic.Bool activeInvocations mapset.Set[*routeHandlerInvocation] } func (r *routeHandlerEntry) Matches(url string) bool { return r.matcher.Matches(url) } func (r *routeHandlerEntry) Handle(route Route) chan bool { handlerInvocation := &routeHandlerInvocation{ route: route, complete: make(chan bool, 1), } r.activeInvocations.Add(handlerInvocation) defer func() { handlerInvocation.complete <- true r.activeInvocations.Remove(handlerInvocation) }() defer func() { // If the handler was stopped (without waiting for completion), we ignore all exceptions. if r.ignoreErrors.Load() { _ = recover() route.(*routeImpl).reportHandled(false) } else { e := recover() if e != nil { err, ok := e.(error) if ok && errors.Is(err, ErrTargetClosed) { panic(fmt.Errorf("\"%w\" while running route callback.\nConsider awaiting `page.UnrouteAll(playwright.PageUnrouteAllOptions{Behavior: playwright.UnrouteBehaviorIgnoreErrors})`\nbefore the end of the test to ignore remaining routes in flight.", err)) } panic(e) } } }() return r.handleInternal(route) } func (r *routeHandlerEntry) Stop(behavior string) { // When a handler is manually unrouted or its page/context is closed we either // - wait for the current handler invocations to finish // - or do not wait, if the user opted out of it, but swallow all exceptions // that happen after the unroute/close. if behavior == string(*UnrouteBehaviorIgnoreErrors) { r.ignoreErrors.Store(true) } else { wg := &sync.WaitGroup{} r.activeInvocations.Each(func(activation *routeHandlerInvocation) bool { if !activation.route.(*routeImpl).didThrow { wg.Add(1) go func(complete chan bool) { <-complete wg.Done() }(activation.complete) } return false }) wg.Wait() } } func (r *routeHandlerEntry) handleInternal(route Route) chan bool { handled := route.(*routeImpl).startHandling() atomic.AddInt32(&r.count, 1) r.handler(route) return handled } func (r *routeHandlerEntry) WillExceed() bool { if r.times == 0 { return false } return int(atomic.LoadInt32(&r.count)+1) >= r.times } func newRouteHandlerEntry(matcher *urlMatcher, handler routeHandler, times ...int) *routeHandlerEntry { n := 0 if len(times) > 0 { n = times[0] } return &routeHandlerEntry{ matcher: matcher, handler: handler, times: n, count: 0, ignoreErrors: &atomic.Bool{}, activeInvocations: mapset.NewSet[*routeHandlerInvocation](), } } func prepareInterceptionPatterns(handlers []*routeHandlerEntry) []map[string]interface{} { patterns := []map[string]interface{}{} all := false for _, h := range handlers { switch h.matcher.raw.(type) { case *regexp.Regexp: pattern, flags := convertRegexp(h.matcher.raw.(*regexp.Regexp)) patterns = append(patterns, map[string]interface{}{ "regexSource": pattern, "regexFlags": flags, }) case string: patterns = append(patterns, map[string]interface{}{ "glob": h.matcher.raw.(string), }) default: all = true } } if all { return []map[string]interface{}{ { "glob": "**/*", }, } } return patterns } const defaultTimeout = 30 * 1000 type timeoutSettings struct { sync.RWMutex parent *timeoutSettings defaultTimeout *float64 defaultNavigationTimeout *float64 } func (t *timeoutSettings) SetDefaultTimeout(timeout *float64) { t.Lock() defer t.Unlock() t.defaultTimeout = timeout } func (t *timeoutSettings) DefaultTimeout() *float64 { t.RLock() defer t.RUnlock() return t.defaultTimeout } func (t *timeoutSettings) Timeout(timeout ...float64) float64 { t.RLock() defer t.RUnlock() if len(timeout) == 1 { return timeout[0] } if t.defaultTimeout != nil { return *t.defaultTimeout } if t.parent != nil { return t.parent.Timeout() } return defaultTimeout } func (t *timeoutSettings) DefaultNavigationTimeout() *float64 { t.RLock() defer t.RUnlock() return t.defaultNavigationTimeout } func (t *timeoutSettings) SetDefaultNavigationTimeout(navigationTimeout *float64) { t.Lock() defer t.Unlock() t.defaultNavigationTimeout = navigationTimeout } func (t *timeoutSettings) NavigationTimeout() float64 { t.RLock() defer t.RUnlock() if t.defaultNavigationTimeout != nil { return *t.defaultNavigationTimeout } if t.parent != nil { return t.parent.NavigationTimeout() } return defaultTimeout } func newTimeoutSettings(parent *timeoutSettings) *timeoutSettings { return &timeoutSettings{ parent: parent, defaultTimeout: nil, defaultNavigationTimeout: nil, } } // SelectOptionValues is the option struct for ElementHandle.Select() etc. type SelectOptionValues struct { ValuesOrLabels *[]string Values *[]string Indexes *[]int Labels *[]string Elements *[]ElementHandle } func convertSelectOptionSet(values SelectOptionValues) map[string]interface{} { out := make(map[string]interface{}) if values == (SelectOptionValues{}) { return out } var o []map[string]interface{} if values.ValuesOrLabels != nil { for _, v := range *values.ValuesOrLabels { m := map[string]interface{}{"valueOrLabel": v} o = append(o, m) } } if values.Values != nil { for _, v := range *values.Values { m := map[string]interface{}{"value": v} o = append(o, m) } } if values.Indexes != nil { for _, i := range *values.Indexes { m := map[string]interface{}{"index": i} o = append(o, m) } } if values.Labels != nil { for _, l := range *values.Labels { m := map[string]interface{}{"label": l} o = append(o, m) } } if o != nil { out["options"] = o } var e []*channel if values.Elements != nil { for _, eh := range *values.Elements { e = append(e, eh.(*elementHandleImpl).channel) } } if e != nil { out["elements"] = e } return out } func unroute(inRoutes []*routeHandlerEntry, url interface{}, handlers ...routeHandler) ([]*routeHandlerEntry, []*routeHandlerEntry, error) { var handler routeHandler if len(handlers) == 1 { handler = handlers[0] } handlerPtr := reflect.ValueOf(handler).Pointer() removed := make([]*routeHandlerEntry, 0) remaining := make([]*routeHandlerEntry, 0) for _, route := range inRoutes { routeHandlerPtr := reflect.ValueOf(route.handler).Pointer() // note: compare regex expression if url is a regexp, not pointer if !route.matcher.SameWith(url) || (handler != nil && routeHandlerPtr != handlerPtr) { remaining = append(remaining, route) } else { removed = append(removed, route) } } return removed, remaining, nil } func serializeMapToNameAndValue(headers map[string]string) []map[string]string { serialized := make([]map[string]string, 0) for name, value := range headers { serialized = append(serialized, map[string]string{ "name": name, "value": value, }) } return serialized } // assignStructFields assigns fields from src to dest, // // omitExtra determines whether to omit src's extra fields func assignStructFields(dest, src interface{}, omitExtra bool) error { destValue := reflect.ValueOf(dest) if destValue.Kind() != reflect.Ptr || destValue.IsNil() { return fmt.Errorf("dest must be a non-nil pointer") } destValue = destValue.Elem() if destValue.Kind() != reflect.Struct { return fmt.Errorf("dest must be a struct") } srcValue := reflect.ValueOf(src) if srcValue.Kind() == reflect.Ptr { srcValue = srcValue.Elem() } if srcValue.Kind() != reflect.Struct { return fmt.Errorf("src must be a struct") } for i := 0; i < destValue.NumField(); i++ { destField := destValue.Field(i) destFieldType := destField.Type() destFieldName := destValue.Type().Field(i).Name if srcField := srcValue.FieldByName(destFieldName); srcField.IsValid() && srcField.Type() != destFieldType { return fmt.Errorf("mismatched field type for field %s", destFieldName) } else if srcField.IsValid() { destField.Set(srcField) } } if !omitExtra { for i := 0; i < srcValue.NumField(); i++ { srcFieldName := srcValue.Type().Field(i).Name if destField := destValue.FieldByName(srcFieldName); !destField.IsValid() { return fmt.Errorf("extra field %s in src", srcFieldName) } } } return nil } func deserializeNameAndValueToMap(headersArray []map[string]string) map[string]string { unserialized := make(map[string]string) for _, item := range headersArray { unserialized[item["name"]] = item["value"] } return unserialized } type recordHarOptions struct { Path string `json:"path"` Content *HarContentPolicy `json:"content,omitempty"` Mode *HarMode `json:"mode,omitempty"` UrlGlob *string `json:"urlGlob,omitempty"` UrlRegexSource *string `json:"urlRegexSource,omitempty"` UrlRegexFlags *string `json:"urlRegexFlags,omitempty"` } type recordHarInputOptions struct { Path string URL interface{} Mode *HarMode Content *HarContentPolicy OmitContent *bool } type harRecordingMetadata struct { Path string Content *HarContentPolicy } func prepareRecordHarOptions(option recordHarInputOptions) recordHarOptions { out := recordHarOptions{ Path: option.Path, } if option.URL != nil { switch option.URL.(type) { case *regexp.Regexp: pattern, flags := convertRegexp(option.URL.(*regexp.Regexp)) out.UrlRegexSource = String(pattern) out.UrlRegexFlags = String(flags) case string: out.UrlGlob = String(option.URL.(string)) } } if option.Mode != nil { out.Mode = option.Mode } if option.Content != nil { out.Content = option.Content } else if option.OmitContent != nil && *option.OmitContent { out.Content = HarContentPolicyOmit } return out } type safeValue[T any] struct { sync.Mutex v T } func (s *safeValue[T]) Set(v T) { s.Lock() defer s.Unlock() s.v = v } func (s *safeValue[T]) Get() T { s.Lock() defer s.Unlock() return s.v }