634 lines
16 KiB
Go
634 lines
16 KiB
Go
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
|
|
}
|