diff --git a/README.md b/README.md index 6c39bc49..a9e3dd7d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/playwright-community/playwright-go)](https://pkg.go.dev/github.com/playwright-community/playwright-go) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/github.com/playwright-community/playwright-go)](https://goreportcard.com/report/github.com/playwright-community/playwright-go) ![Build Status](https://github.com/playwright-community/playwright-go/workflows/Go/badge.svg) -[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack) [![Coverage Status](https://coveralls.io/repos/github/playwright-community/playwright-go/badge.svg?branch=main)](https://coveralls.io/github/playwright-community/playwright-go?branch=main) [![Chromium version](https://img.shields.io/badge/chromium-134.0.6998.35-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-135.0-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.4-blue.svg?logo=safari)](https://webkit.org/) +[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack) [![Coverage Status](https://coveralls.io/repos/github/playwright-community/playwright-go/badge.svg?branch=main)](https://coveralls.io/github/playwright-community/playwright-go?branch=main) [![Chromium version](https://img.shields.io/badge/chromium-136.0.7103.25-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-137.0-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.4-blue.svg?logo=safari)](https://webkit.org/) [API reference](https://playwright.dev/docs/api/class-playwright) | [Example recipes](https://github.com/playwright-community/playwright-go/tree/main/examples) @@ -13,9 +13,9 @@ Playwright is a Go library to automate [Chromium](https://www.chromium.org/Home) | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 134.0.6998.35 | ✅ | ✅ | ✅ | +| Chromium 136.0.7103.25 | ✅ | ✅ | ✅ | | WebKit 18.4 | ✅ | ✅ | ✅ | -| Firefox 135.0 | ✅ | ✅ | ✅ | +| Firefox 137.0 | ✅ | ✅ | ✅ | Headless execution is supported for all the browsers on all platforms. diff --git a/generated-interfaces.go b/generated-interfaces.go index 7ed3733a..187dc910 100644 --- a/generated-interfaces.go +++ b/generated-interfaces.go @@ -382,8 +382,8 @@ type BrowserContext interface { // [this] issue. We recommend disabling Service Workers when // using request interception by setting “[object Object]” to `block`. // - // 1. url: A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a “[object Object]” via the - // context options was provided and the passed URL is a path, it gets merged via the + // 1. url: A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If “[object Object]” is + // set in the context options and the provided URL is a string that does not start with `*`, it is resolved using the // [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. // 2. handler: handler function to route the request. // @@ -529,11 +529,15 @@ type BrowserType interface { // Launches browser that uses persistent storage located at “[object Object]” and returns the only context. Closing // this context will automatically close the browser. // - // userDataDir: Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for + // userDataDir: Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty + // string to create a temporary directory. + // + // More details for // [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction) and - // [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile). Note that Chromium's - // user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty - // string to use a temporary directory instead. + // [Firefox](https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile). Chromium's user data directory is the + // **parent** directory of the "Profile Path" seen at `chrome://version`. + // + // Note that browsers do not allow launching multiple instances with the same User Data Directory. LaunchPersistentContext(userDataDir string, options ...BrowserTypeLaunchPersistentContextOptions) (BrowserContext, error) // Returns browser name. For example: `chromium`, `webkit` or `firefox`. @@ -2906,6 +2910,16 @@ type LocatorAssertions interface { // [visible]: https://playwright.dev/docs/actionability#visible ToBeVisible(options ...LocatorAssertionsToBeVisibleOptions) error + // Ensures the [Locator] points to an element with given CSS classes. All classes from the asserted value, separated + // by spaces, must be present in the + // [Element.ClassList] in any order. + // + // expected: A string containing expected class names, separated by spaces, or a list of such strings to assert multiple + // elements. + // + // [Element.ClassList]: https://developer.mozilla.org/en-US/docs/Web/API/Element/classList + ToContainClass(expected interface{}, options ...LocatorAssertionsToContainClassOptions) error + // Ensures the [Locator] points to an element that contains the given text. All nested elements will be considered // when computing the text content of the element. You can use regular expressions for the value as well. // @@ -2948,7 +2962,7 @@ type LocatorAssertions interface { ToHaveAttribute(name string, value interface{}, options ...LocatorAssertionsToHaveAttributeOptions) error // Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match - // the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: + // the element's `class` attribute. To match individual classes use [LocatorAssertions.ToContainClass]. // // expected: Expected class or RegExp or a list of those. ToHaveClass(expected interface{}, options ...LocatorAssertionsToHaveClassOptions) error @@ -3796,8 +3810,8 @@ type Page interface { // using request interception by setting “[object Object]” to `block`. // **NOTE** [Page.Route] will not intercept the first request of a popup page. Use [BrowserContext.Route] instead. // - // 1. url: A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a “[object Object]” via the - // context options was provided and the passed URL is a path, it gets merged via the + // 1. url: A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If “[object Object]” is + // set in the context options and the provided URL is a string that does not start with `*`, it is resolved using the // [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. // 2. handler: handler function to route the request. // @@ -4376,6 +4390,9 @@ type Route interface { // over to redirected requests. // [Route.Continue] will immediately send the request to the network, other matching handlers won't be invoked. Use // [Route.Fallback] If you want next matching handler in the chain to be invoked. + // **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, + // and the cookie will be loaded from the browser's cookie store. To set custom cookies, use + // [BrowserContext.AddCookies]. Continue(options ...RouteContinueOptions) error // Continues route's request with optional overrides. The method is similar to [Route.Continue] with the difference diff --git a/generated-structs.go b/generated-structs.go index 2e77a6e6..7a90f5a3 100644 --- a/generated-structs.go +++ b/generated-structs.go @@ -35,6 +35,10 @@ type APIRequestNewContextOptions struct { HttpCredentials *HttpCredentials `json:"httpCredentials"` // Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. IgnoreHttpsErrors *bool `json:"ignoreHTTPSErrors"` + // Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + // exceeded. Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request + // individually. + MaxRedirects *int `json:"maxRedirects"` // Network proxy settings. Proxy *Proxy `json:"proxy"` // Populates context with given storage state. This option can be used to initialize context with logged-in @@ -858,7 +862,7 @@ type BrowserTypeLaunchOptions struct { // “[object Object]” option is `true`. // // [Chromium]: https://developers.google.com/web/updates/2017/04/headless-chrome - // [Firefox]: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode + // [Firefox]: https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/ Headless *bool `json:"headless"` // If `true`, Playwright does not pass its own configurations args and only uses the ones from “[object Object]”. // Dangerous option; use with care. Defaults to `false`. @@ -977,7 +981,7 @@ type BrowserTypeLaunchPersistentContextOptions struct { // “[object Object]” option is `true`. // // [Chromium]: https://developers.google.com/web/updates/2017/04/headless-chrome - // [Firefox]: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode + // [Firefox]: https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/ Headless *bool `json:"headless"` // Credentials for [HTTP authentication]. If no // origin is specified, the username and password are sent to any servers upon unauthorized responses. @@ -2254,6 +2258,9 @@ type KeyboardTypeOptions struct { } type LocatorAriaSnapshotOptions struct { + // Generate symbolic reference for each element. One can use `aria-ref=` locator immediately after capturing the + // snapshot to perform actions on the element. + Ref *bool `json:"ref"` // Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can // be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods. Timeout *float64 `json:"timeout"` @@ -2413,14 +2420,14 @@ type LocatorElementHandleOptions struct { } type LocatorEvaluateOptions struct { - // Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - // be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods. + // Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + // evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Timeout *float64 `json:"timeout"` } type LocatorEvaluateHandleOptions struct { - // Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - // be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods. + // Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + // evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Timeout *float64 `json:"timeout"` } @@ -2940,6 +2947,11 @@ type LocatorAssertionsToBeVisibleOptions struct { Visible *bool `json:"visible"` } +type LocatorAssertionsToContainClassOptions struct { + // Time to retry the assertion for in milliseconds. Defaults to `5000`. + Timeout *float64 `json:"timeout"` +} + type LocatorAssertionsToContainTextOptions struct { // Whether to perform case-insensitive match. “[object Object]” option takes precedence over the corresponding regular // expression flag if specified. diff --git a/glob.go b/glob.go index 0175eff9..77deba33 100644 --- a/glob.go +++ b/glob.go @@ -1,7 +1,9 @@ package playwright import ( + "net/url" "regexp" + "strconv" "strings" ) @@ -59,12 +61,6 @@ func globMustToRegex(glob string) *regexp.Regexp { } } else { switch c { - case '?': - tokens = append(tokens, ".") - case '[': - tokens = append(tokens, "[") - case ']': - tokens = append(tokens, "]") case '{': inGroup = true tokens = append(tokens, "(") @@ -75,7 +71,7 @@ func globMustToRegex(glob string) *regexp.Regexp { if inGroup { tokens = append(tokens, "|") } else { - tokens = append(tokens, string(c)) + tokens = append(tokens, "\\"+string(c)) } default: if _, ok := escapedChars[c]; ok { @@ -90,3 +86,85 @@ func globMustToRegex(glob string) *regexp.Regexp { tokens = append(tokens, "$") return regexp.MustCompile(strings.Join(tokens, "")) } + +func resolveGlobToRegex(baseURL *string, glob string, isWebSocketUrl bool) *regexp.Regexp { + if isWebSocketUrl { + baseURL = toWebSocketBaseURL(baseURL) + } + glob = resolveGlobBase(baseURL, glob) + return globMustToRegex(glob) +} + +func resolveGlobBase(baseURL *string, match string) string { + if strings.HasPrefix(match, "*") { + return match + } + + tokenMap := make(map[string]string) + mapToken := func(original string, replacement string) string { + if len(original) == 0 { + return "" + } + tokenMap[replacement] = original + return replacement + } + // Escaped `\\?` behaves the same as `?` in our glob patterns. + match = strings.ReplaceAll(match, `\\?`, "?") + // Glob symbols may be escaped in the URL and some of them such as ? affect resolution, + // so we replace them with safe components first. + relativePath := strings.Split(match, "/") + for i, token := range relativePath { + if token == "." || token == ".." || token == "" { + continue + } + // Handle special case of http*://, note that the new schema has to be + // a web schema so that slashes are properly inserted after domain. + if i == 0 && strings.HasSuffix(token, ":") { + relativePath[i] = mapToken(token, "http:") + } else { + questionIndex := strings.Index(token, "?") + if questionIndex == -1 { + relativePath[i] = mapToken(token, "$_"+strconv.Itoa(i)+"_$") + } else { + newPrefix := mapToken(token[:questionIndex], "$_"+strconv.Itoa(i)+"_$") + newSuffix := mapToken(token[questionIndex:], "?$"+strconv.Itoa(i)+"_$") + relativePath[i] = newPrefix + newSuffix + } + } + } + resolved := constructURLBasedOnBaseURL(baseURL, strings.Join(relativePath, "/")) + for token, original := range tokenMap { + resolved = strings.ReplaceAll(resolved, token, original) + } + return resolved +} + +func constructURLBasedOnBaseURL(baseURL *string, givenURL string) string { + u, err := url.Parse(givenURL) + if err != nil { + return givenURL + } + if baseURL != nil { + base, err := url.Parse(*baseURL) + if err != nil { + return givenURL + } + u = base.ResolveReference(u) + } + if u.Path == "" { // In Node.js, new URL('http://localhost') returns 'http://localhost/'. + u.Path = "/" + } + return u.String() +} + +func toWebSocketBaseURL(baseURL *string) *string { + if baseURL == nil { + return nil + } + + // Allow http(s) baseURL to match ws(s) urls. + re := regexp.MustCompile(`(?m)^http(s?://)`) + wsBaseURL := re.ReplaceAllString(*baseURL, "ws$1") + + return &wsBaseURL +} diff --git a/glob_test.go b/glob_test.go index 709142cc..29a9da69 100644 --- a/glob_test.go +++ b/glob_test.go @@ -51,13 +51,6 @@ func Test_globMustToRegex(t *testing.T) { }, want: true, }, - { - args: args{ - glob: "http://localhost:8080/?imple/path.js", - target: "http://localhost:8080/simple/path.js", - }, - want: true, - }, { args: args{ glob: "**/{a,b}.js", @@ -137,7 +130,35 @@ func Test_globMustToRegex(t *testing.T) { }, { args: args{ - glob: "**/three-columns/settings.html?**id=[a-z]**", + glob: "**/api/v[0-9]", + target: "http://example.com/api/v[0-9]", + }, + want: true, + }, + { + args: args{ + glob: "**/api/v[0-9]", + target: "http://example.com/api/version", + }, + want: false, + }, + { + args: args{ + glob: "**/api\\?param", + target: "http://example.com/api?param", + }, + want: true, + }, + { + args: args{ + glob: "**/api\\?param", + target: "http://example.com/api-param", + }, + want: false, + }, + { + args: args{ + glob: "**/three-columns/settings.html\\?**id=settings-**", target: "http://mydomain:8080/blah/blah/three-columns/settings.html?id=settings-e3c58efe-02e9-44b0-97ac-dd138100cf7c&blah", }, want: true, @@ -155,6 +176,29 @@ func Test_globMustToRegex(t *testing.T) { require.Equal(t, globMustToRegex("\\").String(), `^\\$`) require.Equal(t, globMustToRegex("\\\\").String(), `^\\$`) require.Equal(t, globMustToRegex("\\[").String(), `^\[$`) - require.Equal(t, globMustToRegex("[a-z]").String(), `^[a-z]$`) + require.Equal(t, globMustToRegex("[a-z]").String(), `^\[a-z\]$`) require.Equal(t, globMustToRegex("$^+.\\*()|\\?\\{\\}\\[\\]").String(), `^\$\^\+\.\*\(\)\|\?\{\}\[\]$`) } + +func TestURLMatches(t *testing.T) { + require.True(t, newURLMatcher("http://playwright.dev", nil).Matches("http://playwright.dev/")) + require.True(t, newURLMatcher("http://playwright.dev?a=b", nil).Matches("http://playwright.dev/?a=b")) + require.True(t, newURLMatcher("h*://playwright.dev", nil).Matches("http://playwright.dev/")) + require.True(t, newURLMatcher("http://*.playwright.dev?x=y", nil).Matches("http://api.playwright.dev/?x=y")) + require.True(t, newURLMatcher("**/foo/**", nil).Matches("http://playwright.dev/foo/bar")) + require.True(t, newURLMatcher("?x=y", String("http://playwright.dev")).Matches("http://playwright.dev/?x=y")) + require.True(t, newURLMatcher("./bar?x=y", String("http://playwright.dev/foo/")).Matches("http://playwright.dev/foo/bar?x=y")) + + // This is not supported, we treat ? as a query separator. + require.False(t, globMustToRegex("http://localhost:8080/?imple/path.js").MatchString("http://localhost:8080/Simple/path.js")) + require.False(t, newURLMatcher("http://playwright.?ev", nil).Matches("http://playwright.dev/")) + require.True(t, newURLMatcher("http://playwright.?ev", nil).Matches("http://playwright./?ev")) + require.False(t, newURLMatcher("http://playwright.dev/f??", nil).Matches("http://playwright.dev/foo")) + require.True(t, newURLMatcher("http://playwright.dev/f??", nil).Matches("http://playwright.dev/f??")) + require.True(t, newURLMatcher("http://playwright.dev\\?x=y", nil).Matches("http://playwright.dev/?x=y")) + require.True(t, newURLMatcher("http://playwright.dev/\\?x=y", nil).Matches("http://playwright.dev/?x=y")) + require.True(t, newURLMatcher("?bar", String("http://playwright.dev/foo")).Matches("http://playwright.dev/foo?bar")) + require.True(t, newURLMatcher("\\\\?bar", String("http://playwright.dev/foo")).Matches("http://playwright.dev/foo?bar")) + require.True(t, newURLMatcher("**/foo", String("http://first.host/")).Matches("http://second.host/foo")) + require.True(t, newURLMatcher("*//localhost/", String("http://playwright.dev/")).Matches("http://localhost/")) +} diff --git a/go.mod b/go.mod index 7104340c..c9d37a79 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.22 require ( github.com/coder/websocket v1.8.12 - github.com/deckarep/golang-set/v2 v2.6.0 - github.com/go-jose/go-jose/v3 v3.0.3 + github.com/deckarep/golang-set/v2 v2.7.0 + github.com/go-jose/go-jose/v3 v3.0.4 github.com/go-stack/stack v1.8.1 github.com/h2non/filetype v1.1.3 github.com/mitchellh/go-ps v1.0.0 diff --git a/go.sum b/go.sum index 87fe88ea..cbb02474 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= -github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k= +github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/helpers.go b/helpers.go index c28ea07e..b2244f16 100644 --- a/helpers.go +++ b/helpers.go @@ -3,7 +3,6 @@ package playwright import ( "errors" "fmt" - "path" "reflect" "regexp" "strings" @@ -180,19 +179,15 @@ type urlMatcher struct { matchFn func(url string) bool } -func newURLMatcher(urlOrPredicate, baseURL interface{}) *urlMatcher { +func newURLMatcher(urlOrPredicate interface{}, baseURL *string, isWsUrl ...bool) *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: resolveGlobToRegex(baseURL, v, len(isWsUrl) > 0 && isWsUrl[0]), + raw: urlOrPredicate, } - return &urlMatcher{pattern: globMustToRegex(url), raw: urlOrPredicate} } fn, ok := urlOrPredicate.(func(string) bool) if ok { diff --git a/js_handle.go b/js_handle.go index 58ed33bf..6aaa68d6 100644 --- a/js_handle.go +++ b/js_handle.go @@ -1,6 +1,9 @@ package playwright import ( + "bytes" + "encoding/base64" + "encoding/binary" "errors" "fmt" "math" @@ -180,6 +183,59 @@ func parseValue(result interface{}, refs map[float64]interface{}) interface{} { Stack: v.(map[string]interface{})["s"].(string), }) } + if v, ok := vMap["ta"]; ok { + b, b_ok := v.(map[string]interface{})["b"].(string) + k, k_ok := v.(map[string]interface{})["k"].(string) + if b_ok && k_ok { + decoded, err := base64.StdEncoding.DecodeString(b) + if err != nil { + panic(fmt.Errorf("Unexpected value: %v", vMap)) + } + r := bytes.NewReader(decoded) + switch k { + case "i8": + result := make([]int8, len(decoded)) + return mustReadArray(r, &result) + case "ui8", "ui8c": + result := make([]uint8, len(decoded)) + return mustReadArray(r, &result) + case "i16": + size := mustBeDivisible(len(decoded), 2) + result := make([]int16, size) + return mustReadArray(r, &result) + case "ui16": + size := mustBeDivisible(len(decoded), 2) + result := make([]uint16, size) + return mustReadArray(r, &result) + case "i32": + size := mustBeDivisible(len(decoded), 4) + result := make([]int32, size) + return mustReadArray(r, &result) + case "ui32": + size := mustBeDivisible(len(decoded), 4) + result := make([]uint32, size) + return mustReadArray(r, &result) + case "f32": + size := mustBeDivisible(len(decoded), 4) + result := make([]float32, size) + return mustReadArray(r, &result) + case "f64": + size := mustBeDivisible(len(decoded), 8) + result := make([]float64, size) + return mustReadArray(r, &result) + case "bi64": + size := mustBeDivisible(len(decoded), 8) + result := make([]int64, size) + return mustReadArray(r, &result) + case "bui64": + size := mustBeDivisible(len(decoded), 8) + result := make([]uint64, size) + return mustReadArray(r, &result) + default: + panic(fmt.Errorf("Unsupported array type: %s", k)) + } + } + } panic(fmt.Errorf("Unexpected value: %v", vMap)) } @@ -344,3 +400,22 @@ func newJSHandle(parent *channelOwner, objectType string, guid string, initializ }) return bt } + +func mustBeDivisible(length int, wordSize int) int { + if length%wordSize != 0 { + panic(fmt.Errorf(`Decoded bytes length %d is not a multiple of word size %d`, length, wordSize)) + } + return length / wordSize +} + +func mustReadArray[T int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | float32 | float64](r *bytes.Reader, v *[]T) []float64 { + err := binary.Read(r, binary.LittleEndian, v) + if err != nil { + panic(err) + } + data := make([]float64, len(*v)) + for i, v := range *v { + data[i] = float64(v) + } + return data +} diff --git a/locator_assertions.go b/locator_assertions.go index 4d722427..e2ea2bbe 100644 --- a/locator_assertions.go +++ b/locator_assertions.go @@ -180,6 +180,45 @@ func (la *locatorAssertionsImpl) ToBeVisible(options ...LocatorAssertionsToBeVis ) } +func (la *locatorAssertionsImpl) ToContainClass(expected interface{}, options ...LocatorAssertionsToContainClassOptions) error { + var timeout *float64 + if len(options) == 1 { + timeout = options[0].Timeout + } + switch expected.(type) { + case []string: + expectedText, err := toExpectedTextValues(convertToInterfaceList(expected), false, false, nil) + if err != nil { + return err + } + return la.expect( + "to.contain.class.array", + frameExpectOptions{ + ExpectedText: expectedText, + Timeout: timeout, + }, + expected, + "Locator expected to contain class", + ) + case string: + expectedText, err := toExpectedTextValues([]interface{}{expected}, false, false, nil) + if err != nil { + return err + } + return la.expect( + "to.contain.class", + frameExpectOptions{ + ExpectedText: expectedText, + Timeout: timeout, + }, + expected, + "Locator expected to contain class", + ) + default: + return fmt.Errorf("expected should be string or []string, but got %T", expected) + } +} + func (la *locatorAssertionsImpl) ToContainText(expected interface{}, options ...LocatorAssertionsToContainTextOptions) error { var ( timeout *float64 diff --git a/page.go b/page.go index d533bfcc..d4271a04 100644 --- a/page.go +++ b/page.go @@ -1355,7 +1355,7 @@ func (p *pageImpl) RequestGC() error { func (p *pageImpl) RouteWebSocket(url interface{}, handler func(WebSocketRoute)) error { p.Lock() defer p.Unlock() - p.webSocketRoutes = slices.Insert(p.webSocketRoutes, 0, newWebSocketRouteHandler(newURLMatcher(url, p.browserContext.options.BaseURL), handler)) + p.webSocketRoutes = slices.Insert(p.webSocketRoutes, 0, newWebSocketRouteHandler(newURLMatcher(url, p.browserContext.options.BaseURL, true), handler)) return p.updateWebSocketInterceptionPatterns() } diff --git a/patches/main.patch b/patches/main.patch index e5a6f915..4ca27257 100644 --- a/patches/main.patch +++ b/patches/main.patch @@ -1,8 +1,8 @@ diff --git a/docs/src/api/class-apirequest.md b/docs/src/api/class-apirequest.md -index c984b9f12..8a0ed6ed0 100644 +index d27e22b03..e2f2eb063 100644 --- a/docs/src/api/class-apirequest.md +++ b/docs/src/api/class-apirequest.md -@@ -55,7 +55,7 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat +@@ -62,7 +62,7 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat ### option: APIRequest.newContext.storageState * since: v1.16 @@ -243,10 +243,10 @@ index c96e01991..5d5c232f4 100644 Set to `true` to include IndexedDB in the storage state snapshot. diff --git a/docs/src/api/class-apiresponse.md b/docs/src/api/class-apiresponse.md -index 5a901b76b..26da2aa12 100644 +index 468a5a830..02fce8852 100644 --- a/docs/src/api/class-apiresponse.md +++ b/docs/src/api/class-apiresponse.md -@@ -65,7 +65,7 @@ Headers with multiple entries, such as `Set-Cookie`, appear in the array multipl +@@ -66,7 +66,7 @@ Headers with multiple entries, such as `Set-Cookie`, appear in the array multipl ## async method: APIResponse.json * since: v1.16 @@ -330,7 +330,7 @@ index 7867ce5c8..6a27ede39 100644 :::note diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md -index 311b786da..f03926646 100644 +index afd73a414..bf3f5ee0a 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -417,7 +417,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte @@ -342,7 +342,7 @@ index 311b786da..f03926646 100644 - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the current working directory. Optional. -@@ -1216,7 +1216,7 @@ handler function to route the request. +@@ -1214,7 +1214,7 @@ handler function to route the request. ### param: BrowserContext.route.handler * since: v1.8 @@ -351,7 +351,7 @@ index 311b786da..f03926646 100644 - `handler` <[function]\([Route]\)> handler function to route the request. -@@ -1359,7 +1359,7 @@ Handler function to route the WebSocket. +@@ -1357,7 +1357,7 @@ Handler function to route the WebSocket. ### param: BrowserContext.routeWebSocket.handler * since: v1.48 @@ -360,7 +360,7 @@ index 311b786da..f03926646 100644 - `handler` <[function]\([WebSocketRoute]\)> Handler function to route the WebSocket. -@@ -1367,7 +1367,7 @@ Handler function to route the WebSocket. +@@ -1365,7 +1365,7 @@ Handler function to route the WebSocket. ## method: BrowserContext.serviceWorkers * since: v1.11 @@ -369,7 +369,7 @@ index 311b786da..f03926646 100644 - returns: <[Array]<[Worker]>> :::note -@@ -1524,6 +1524,7 @@ Returns storage state for this browser context, contains current cookies, local +@@ -1522,6 +1522,7 @@ Returns storage state for this browser context, contains current cookies, local ### option: BrowserContext.storageState.indexedDB * since: v1.51 @@ -377,7 +377,7 @@ index 311b786da..f03926646 100644 - `indexedDB` ? Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot. -@@ -1565,6 +1566,13 @@ A glob pattern, regex pattern or predicate receiving [URL] used to register a ro +@@ -1559,6 +1560,13 @@ A glob pattern, regex pattern or predicate receiving [URL] used to register a ro Optional handler function used to register a routing with [`method: BrowserContext.route`]. @@ -391,7 +391,7 @@ index 311b786da..f03926646 100644 ### param: BrowserContext.unroute.handler * since: v1.8 * langs: csharp, java -@@ -1606,7 +1614,8 @@ Condition to wait for. +@@ -1600,7 +1608,8 @@ Condition to wait for. ## async method: BrowserContext.waitForConsoleMessage * since: v1.34 @@ -401,7 +401,7 @@ index 311b786da..f03926646 100644 - alias-python: expect_console_message - alias-csharp: RunAndWaitForConsoleMessage - returns: <[ConsoleMessage]> -@@ -1637,7 +1646,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti +@@ -1631,7 +1640,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti ## async method: BrowserContext.waitForEvent * since: v1.8 @@ -411,7 +411,7 @@ index 311b786da..f03926646 100644 - alias-python: expect_event - returns: <[any]> -@@ -1703,7 +1713,8 @@ Either a predicate that receives an event or an options object. Optional. +@@ -1697,7 +1707,8 @@ Either a predicate that receives an event or an options object. Optional. ## async method: BrowserContext.waitForPage * since: v1.9 @@ -421,7 +421,7 @@ index 311b786da..f03926646 100644 - alias-python: expect_page - alias-csharp: RunAndWaitForPage - returns: <[Page]> -@@ -1722,7 +1733,7 @@ Will throw an error if the context closes before new [Page] is created. +@@ -1716,7 +1727,7 @@ Will throw an error if the context closes before new [Page] is created. ### option: BrowserContext.waitForPage.predicate * since: v1.9 @@ -430,7 +430,7 @@ index 311b786da..f03926646 100644 - `predicate` <[function]\([Page]\):[boolean]> Receives the [Page] object and resolves to truthy value when the waiting should resolve. -@@ -1735,7 +1746,8 @@ Receives the [Page] object and resolves to truthy value when the waiting should +@@ -1729,7 +1740,8 @@ Receives the [Page] object and resolves to truthy value when the waiting should ## async method: BrowserContext.waitForEvent2 * since: v1.8 @@ -553,8 +553,30 @@ index e286c63bf..7dbd5f4fd 100644 * alias-python: expect_navigation * alias-csharp: RunAndWaitForNavigation - returns: <[null]|[Response]> +diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md +index 25462a0ed..f85e4aff6 100644 +--- a/docs/src/api/class-locator.md ++++ b/docs/src/api/class-locator.md +@@ -885,7 +885,7 @@ Optional argument to pass to [`param: expression`]. + + ### option: Locator.evaluate.timeout + * since: v1.14 +-* langs: python, java, csharp ++* langs: python, java, csharp, go + - `timeout` <[float]> + + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. +@@ -982,7 +982,7 @@ Optional argument to pass to [`param: expression`]. + + ### option: Locator.evaluateHandle.timeout + * since: v1.14 +-* langs: python, java, csharp ++* langs: python, java, csharp, go + - `timeout` <[float]> + + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md -index 59618d0fb..32d94778d 100644 +index 78ed036c0..e78149d3d 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -67,7 +67,7 @@ public class ExampleTests : PageTest @@ -566,7 +588,7 @@ index 59618d0fb..32d94778d 100644 - returns: <[LocatorAssertions]> Makes the assertion check for the opposite condition. For example, this code tests that the Locator doesn't contain text `"error"`: -@@ -1167,7 +1167,7 @@ Expected substring or RegExp or a list of those. +@@ -1283,7 +1283,7 @@ Expected substring or RegExp or a list of those. ### param: LocatorAssertions.toContainText.expected * since: v1.18 @@ -575,7 +597,7 @@ index 59618d0fb..32d94778d 100644 - `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> Expected substring or RegExp or a list of those. -@@ -1511,7 +1511,7 @@ Expected class or RegExp or a list of those. +@@ -1627,7 +1627,7 @@ Expected class or RegExp or a list of those. ### param: LocatorAssertions.toHaveClass.expected * since: v1.18 @@ -584,7 +606,7 @@ index 59618d0fb..32d94778d 100644 - `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> Expected class or RegExp or a list of those. -@@ -2038,7 +2038,7 @@ Expected string or RegExp or a list of those. +@@ -2154,7 +2154,7 @@ Expected string or RegExp or a list of those. ### param: LocatorAssertions.toHaveText.expected * since: v1.18 @@ -593,7 +615,7 @@ index 59618d0fb..32d94778d 100644 - `expected` <[string]|[RegExp]|[Array]<[string]>|[Array]<[RegExp]>|[Array]<[string]|[RegExp]>> Expected string or RegExp or a list of those. -@@ -2172,7 +2172,7 @@ await Expect(locator).ToHaveValuesAsync(new Regex[] { new Regex("R"), new Regex( +@@ -2288,7 +2288,7 @@ await Expect(locator).ToHaveValuesAsync(new Regex[] { new Regex("R"), new Regex( ### param: LocatorAssertions.toHaveValues.values * since: v1.23 @@ -603,7 +625,7 @@ index 59618d0fb..32d94778d 100644 Expected options currently selected. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md -index b59f2cb3f..8ad144d44 100644 +index a69ac2446..a026da311 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -621,7 +621,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte @@ -740,7 +762,7 @@ index b59f2cb3f..8ad144d44 100644 * since: v1.42 - `handler` <[function]\([Locator]\)> -@@ -3618,6 +3655,13 @@ it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/We +@@ -3616,6 +3653,13 @@ A glob pattern, regex pattern, or predicate that receives a [URL] to match durin handler function to route the request. @@ -754,7 +776,7 @@ index b59f2cb3f..8ad144d44 100644 ### param: Page.route.handler * since: v1.8 * langs: csharp, java -@@ -3752,7 +3796,7 @@ Handler function to route the WebSocket. +@@ -3750,7 +3794,7 @@ Handler function to route the WebSocket. ### param: Page.routeWebSocket.handler * since: v1.48 @@ -763,7 +785,7 @@ index b59f2cb3f..8ad144d44 100644 - `handler` <[function]\([WebSocketRoute]\)> Handler function to route the WebSocket. -@@ -4081,14 +4125,14 @@ await page.GotoAsync("https://www.microsoft.com"); +@@ -4079,14 +4123,14 @@ await page.GotoAsync("https://www.microsoft.com"); ### param: Page.setViewportSize.width * since: v1.10 @@ -780,7 +802,7 @@ index b59f2cb3f..8ad144d44 100644 - `height` <[int]> Page height in pixels. -@@ -4275,6 +4319,13 @@ A glob pattern, regex pattern or predicate receiving [URL] to match while routin +@@ -4273,6 +4317,13 @@ A glob pattern, regex pattern or predicate receiving [URL] to match while routin Optional handler function to route the request. @@ -794,7 +816,7 @@ index b59f2cb3f..8ad144d44 100644 ### param: Page.unroute.handler * since: v1.8 * langs: csharp, java -@@ -4313,7 +4364,8 @@ Performs action and waits for the Page to close. +@@ -4311,7 +4362,8 @@ Performs action and waits for the Page to close. ## async method: Page.waitForConsoleMessage * since: v1.9 @@ -804,7 +826,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_console_message - alias-csharp: RunAndWaitForConsoleMessage - returns: <[ConsoleMessage]> -@@ -4344,7 +4396,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti +@@ -4342,7 +4394,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti ## async method: Page.waitForDownload * since: v1.9 @@ -814,7 +836,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_download - alias-csharp: RunAndWaitForDownload - returns: <[Download]> -@@ -4375,7 +4428,8 @@ Receives the [Download] object and resolves to truthy value when the waiting sho +@@ -4373,7 +4426,8 @@ Receives the [Download] object and resolves to truthy value when the waiting sho ## async method: Page.waitForEvent * since: v1.8 @@ -824,7 +846,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_event - returns: <[any]> -@@ -4428,7 +4482,8 @@ Either a predicate that receives an event or an options object. Optional. +@@ -4426,7 +4480,8 @@ Either a predicate that receives an event or an options object. Optional. ## async method: Page.waitForFileChooser * since: v1.9 @@ -834,7 +856,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_file_chooser - alias-csharp: RunAndWaitForFileChooser - returns: <[FileChooser]> -@@ -4586,7 +4641,7 @@ await page.WaitForFunctionAsync("selector => !!document.querySelector(selector)" +@@ -4584,7 +4639,7 @@ await page.WaitForFunctionAsync("selector => !!document.querySelector(selector)" Optional argument to pass to [`param: expression`]. @@ -843,7 +865,7 @@ index b59f2cb3f..8ad144d44 100644 * since: v1.8 ### option: Page.waitForFunction.polling = %%-csharp-java-wait-for-function-polling-%% -@@ -4683,6 +4738,11 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. +@@ -4681,6 +4736,11 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. ``` ### param: Page.waitForLoadState.state = %%-wait-for-load-state-state-%% @@ -855,7 +877,7 @@ index b59f2cb3f..8ad144d44 100644 * since: v1.8 ### option: Page.waitForLoadState.timeout = %%-navigation-timeout-%% -@@ -4695,6 +4755,7 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. +@@ -4693,6 +4753,7 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. * since: v1.8 * deprecated: This method is inherently racy, please use [`method: Page.waitForURL`] instead. * langs: @@ -863,7 +885,7 @@ index b59f2cb3f..8ad144d44 100644 * alias-python: expect_navigation * alias-csharp: RunAndWaitForNavigation - returns: <[null]|[Response]> -@@ -4779,7 +4840,8 @@ a navigation. +@@ -4777,7 +4838,8 @@ a navigation. ## async method: Page.waitForPopup * since: v1.9 @@ -873,7 +895,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_popup - alias-csharp: RunAndWaitForPopup - returns: <[Page]> -@@ -4811,6 +4873,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should +@@ -4809,6 +4871,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should ## async method: Page.waitForRequest * since: v1.8 * langs: @@ -881,7 +903,7 @@ index b59f2cb3f..8ad144d44 100644 * alias-python: expect_request * alias-csharp: RunAndWaitForRequest - returns: <[Request]> -@@ -4918,7 +4981,8 @@ changed by using the [`method: Page.setDefaultTimeout`] method. +@@ -4916,7 +4979,8 @@ changed by using the [`method: Page.setDefaultTimeout`] method. ## async method: Page.waitForRequestFinished * since: v1.12 @@ -891,7 +913,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_request_finished - alias-csharp: RunAndWaitForRequestFinished - returns: <[Request]> -@@ -4950,6 +5014,7 @@ Receives the [Request] object and resolves to truthy value when the waiting shou +@@ -4948,6 +5012,7 @@ Receives the [Request] object and resolves to truthy value when the waiting shou ## async method: Page.waitForResponse * since: v1.8 * langs: @@ -899,7 +921,7 @@ index b59f2cb3f..8ad144d44 100644 * alias-python: expect_response * alias-csharp: RunAndWaitForResponse - returns: <[Response]> -@@ -5313,7 +5378,8 @@ await page.WaitForURLAsync("**/target.html"); +@@ -5311,7 +5376,8 @@ await page.WaitForURLAsync("**/target.html"); ## async method: Page.waitForWebSocket * since: v1.9 @@ -909,7 +931,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_websocket - alias-csharp: RunAndWaitForWebSocket - returns: <[WebSocket]> -@@ -5344,7 +5410,8 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh +@@ -5342,7 +5408,8 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh ## async method: Page.waitForWorker * since: v1.9 @@ -919,7 +941,7 @@ index b59f2cb3f..8ad144d44 100644 - alias-python: expect_worker - alias-csharp: RunAndWaitForWorker - returns: <[Worker]> -@@ -5386,7 +5453,8 @@ This does not contain ServiceWorkers +@@ -5384,7 +5451,8 @@ This does not contain ServiceWorkers ## async method: Page.waitForEvent2 * since: v1.8 @@ -1051,10 +1073,10 @@ index cddaf6965..6aa3225f5 100644 Returns the JSON representation of response body. diff --git a/docs/src/api/class-route.md b/docs/src/api/class-route.md -index c4e5fcb2e..59c9c939d 100644 +index dbbe12149..db1c96d14 100644 --- a/docs/src/api/class-route.md +++ b/docs/src/api/class-route.md -@@ -127,7 +127,7 @@ If set changes the post data of request. +@@ -131,7 +131,7 @@ If set changes the post data of request. ### option: Route.continue.postData * since: v1.8 @@ -1063,7 +1085,7 @@ index c4e5fcb2e..59c9c939d 100644 - `postData` <[string]|[Buffer]> If set changes the post data of request. -@@ -414,7 +414,7 @@ If set changes the post data of request. +@@ -418,7 +418,7 @@ If set changes the post data of request. ### option: Route.fallback.postData * since: v1.23 @@ -1072,7 +1094,7 @@ index c4e5fcb2e..59c9c939d 100644 - `postData` <[string]|[Buffer]> If set changes the post data of request. -@@ -537,7 +537,7 @@ and `content-type` header will be set to `application/json` if not explicitly se +@@ -541,7 +541,7 @@ and `content-type` header will be set to `application/json` if not explicitly se set to `application/octet-stream` if not explicitly set. ### option: Route.fetch.postData @@ -1081,7 +1103,7 @@ index c4e5fcb2e..59c9c939d 100644 * since: v1.29 - `postData` <[string]|[Buffer]> -@@ -650,7 +650,7 @@ If set, equals to setting `Content-Type` response header. +@@ -654,7 +654,7 @@ If set, equals to setting `Content-Type` response header. ### option: Route.fulfill.body * since: v1.8 @@ -1196,7 +1218,7 @@ index e23316ebc..ae3d20b9f 100644 * since: v1.48 * langs: csharp, java diff --git a/docs/src/api/params.md b/docs/src/api/params.md -index fd548c9c4..99dd9f4c1 100644 +index 4c4937004..ea6212dc9 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -8,7 +8,7 @@ When to consider operation succeeded, defaults to `load`. Events can be either: @@ -1572,10 +1594,10 @@ index fd548c9c4..99dd9f4c1 100644 Firefox user preferences. Learn more about the Firefox user preferences at diff --git a/utils/doclint/generateGoApi.js b/utils/doclint/generateGoApi.js new file mode 100644 -index 000000000..699b44433 +index 000000000..cd5f22cca --- /dev/null +++ b/utils/doclint/generateGoApi.js -@@ -0,0 +1,868 @@ +@@ -0,0 +1,871 @@ +/** + * Copyright (c) Microsoft Corporation. + * @@ -2282,6 +2304,8 @@ index 000000000..699b44433 + return 'interface{}'; + else if (type.expression === '[function]([null]|[int], [null]|[string])') + return 'func(*int, *string)'; ++ else if (optional && type.expression === '[string]|[Array]<[string]>') ++ return '[]string' // result: ...string + + let isNullableEnum = false; + if (type.union) { @@ -2329,8 +2353,9 @@ index 000000000..699b44433 + || type.expression === '[string]|[float]|[boolean]') { + console.warn(`${type.name} should be a 'string', but was a ${type.expression}`); + return `string`; -+ } else if (type.union.length == 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name) -+ return `[]${type.union[0].name}`; // an example of this is [string]|[Array]<[string]> ++ } ++ // else if (type.union.length == 2 && type.union[1].name === 'Array' && type.union[1].templates[0].name === type.union[0].name) ++ // return `[]${type.union[0].name}`; // an example of this is [string]|[Array]<[string]> + else if (type.union[0].name === 'path') + // we don't support path, but we know it's usually an object on the other end, and we expect + // the dotnet folks to use [NameOfTheObject].LoadFromPath(); method which we can provide separately diff --git a/playwright b/playwright index 0ad26b38..471930b1 160000 --- a/playwright +++ b/playwright @@ -1 +1 @@ -Subproject commit 0ad26b38902449d9347536c97a34cc5dedbec729 +Subproject commit 471930b1ceae03c9e66e0eb80c1364a1a788e7db diff --git a/run.go b/run.go index a41a1f45..79ad11ef 100644 --- a/run.go +++ b/run.go @@ -16,7 +16,7 @@ import ( "strings" ) -const playwrightCliVersion = "1.51.1" +const playwrightCliVersion = "1.52.0" var ( logger = slog.Default() diff --git a/tests/fetch_test.go b/tests/fetch_test.go index 4196ab68..9f4ef00e 100644 --- a/tests/fetch_test.go +++ b/tests/fetch_test.go @@ -596,3 +596,23 @@ func TestFetchShouldNotThrowWhenFailOnStatusCodeIsFalse(t *testing.T) { require.Equal(t, 404, resp.Status()) require.NoError(t, req.Dispose()) } + +func TestShouldFollowMaxRedirects(t *testing.T) { + BeforeEach(t) + + redirectCount := atomic.Int32{} + server.SetRoute("/empty.html", func(w http.ResponseWriter, r *http.Request) { + redirectCount.Add(1) + w.Header().Add("Location", server.EMPTY_PAGE) + w.WriteHeader(301) + }) + + request, err := pw.Request.NewContext(playwright.APIRequestNewContextOptions{ + MaxRedirects: playwright.Int(1), + }) + require.NoError(t, err) + _, err = request.Fetch(server.EMPTY_PAGE) + require.ErrorContains(t, err, "Max redirect count exceeded") + require.Equal(t, int32(2), redirectCount.Load()) + require.NoError(t, request.Dispose()) +} diff --git a/tests/js_handle_test.go b/tests/js_handle_test.go index 8590544d..265d4dfc 100644 --- a/tests/js_handle_test.go +++ b/tests/js_handle_test.go @@ -2,6 +2,7 @@ package playwright_test import ( "errors" + "fmt" "math" "math/big" "net/url" @@ -407,3 +408,27 @@ func TestEvaluate(t *testing.T) { require.Equal(t, nil, val) }) } + +func TestEvaluateTransferTypedArrays(t *testing.T) { + BeforeEach(t) + + testTypedArray := func(t *testing.T, typedArray string, expected []float64, valueSuffix string) { + t.Run(typedArray, func(t *testing.T) { + val, err := page.Evaluate(fmt.Sprintf(`() => new %s([1%s, 2%s, 3%s])`, typedArray, valueSuffix, valueSuffix, valueSuffix)) + require.NoError(t, err) + require.Equal(t, expected, val.([]float64)) + }) + } + + testTypedArray(t, "Int8Array", []float64{1, 2, 3}, "") + testTypedArray(t, "Uint8Array", []float64{1, 2, 3}, "") + testTypedArray(t, "Uint8ClampedArray", []float64{1, 2, 3}, "") + testTypedArray(t, "Int16Array", []float64{1, 2, 3}, "") + testTypedArray(t, "Uint16Array", []float64{1, 2, 3}, "") + testTypedArray(t, "Int32Array", []float64{1, 2, 3}, "") + testTypedArray(t, "Uint32Array", []float64{1, 2, 3}, "") + testTypedArray(t, "Float32Array", []float64{1.5, 2.5, 3.5}, ".5") + testTypedArray(t, "Float64Array", []float64{1.5, 2.5, 3.5}, ".5") + testTypedArray(t, "BigInt64Array", []float64{1, 2, 3}, "n") + testTypedArray(t, "BigUint64Array", []float64{1, 2, 3}, "n") +} diff --git a/tests/locator_assertions_test.go b/tests/locator_assertions_test.go index 03b09725..668fda7e 100644 --- a/tests/locator_assertions_test.go +++ b/tests/locator_assertions_test.go @@ -478,3 +478,23 @@ func TestLocatorToHaveRole(t *testing.T) { require.NoError(t, expect.Locator(locator).ToHaveRole("button")) require.NoError(t, expect.Locator(locator).Not().ToHaveRole("checkbox")) } + +func TestLocatorToContainClass(t *testing.T) { + BeforeEach(t) + + err := page.SetContent(`
`) + require.NoError(t, err) + + locator := page.Locator("div") + require.NoError(t, expect.Locator(locator).ToContainClass("")) + require.NoError(t, expect.Locator(locator).ToContainClass("bar")) + require.NoError(t, expect.Locator(locator).ToContainClass("baz bar")) + require.NoError(t, expect.Locator(locator).Not().ToContainClass(" baz not-matching ")) + + err = page.SetContent(`
`) + require.NoError(t, err) + + require.NoError(t, expect.Locator(locator).ToContainClass([]string{"foo", "hello", "baz"})) + require.NoError(t, expect.Locator(locator).Not().ToContainClass([]string{"not-there", "hello", "baz"})) // Class not there + require.NoError(t, expect.Locator(locator).Not().ToContainClass([]string{"foo", "hello"})) // Length mismatch +} diff --git a/tests/page_aria_snapshot_test.go b/tests/page_aria_snapshot_test.go index 126026ce..b5266967 100644 --- a/tests/page_aria_snapshot_test.go +++ b/tests/page_aria_snapshot_test.go @@ -94,6 +94,82 @@ func TestShouldSnapshotComplex(t *testing.T) { checkAndMatchSnapshot(t, page.Locator("body"), ` - list: - listitem: - - link "link" + - link "link": + - /url: about:blank `) } + +func TestShouldSnapshotWithRef(t *testing.T) { + BeforeEach(t) + + require.NoError(t, page.SetContent(``)) + expected := Unshift(` + - list [ref=s1e3]: + - listitem [ref=s1e4]: + - link "link" [ref=s1e5]: + - /url: about:blank + `) + ariaSnapshot, err := page.Locator("body").AriaSnapshot(playwright.LocatorAriaSnapshotOptions{ + Ref: playwright.Bool(true), + }) + require.NoError(t, err) + require.Equal(t, expected, ariaSnapshot) +} + +func TestShouldSnapshotWithUnexpectedChildrenEqual(t *testing.T) { + BeforeEach(t) + + require.NoError(t, page.SetContent(` + + `)) + require.NoError(t, expect.Locator(page.Locator("body")).ToMatchAriaSnapshot(Unshift(` + - list: + - listitem: One + - listitem: Three + `))) + require.Error(t, expect.Locator(page.Locator("body")).ToMatchAriaSnapshot(Unshift(` + - list: + - /children: equal + - listitem: One + - listitem: Three + `), playwright.LocatorAssertionsToMatchAriaSnapshotOptions{Timeout: playwright.Float(1000)})) +} + +func TestShouldSnapshotWithUnexpectedChildrenDeepEqual(t *testing.T) { + BeforeEach(t) + + require.NoError(t, page.SetContent(` + + `)) + require.NoError(t, expect.Locator(page.Locator("body")).ToMatchAriaSnapshot(` + - list: + - listitem: + - list: + - listitem: 1.1 + `)) + require.NoError(t, expect.Locator(page.Locator("body")).ToMatchAriaSnapshot(` + - list: + - /children: equal + - listitem: + - list: + - listitem: 1.1 + `)) + require.Error(t, expect.Locator(page.Locator("body")).ToMatchAriaSnapshot(` + - list: + - /children: deep-equal + - listitem: + - list: + - listitem: 1.1 + `, playwright.LocatorAssertionsToMatchAriaSnapshotOptions{Timeout: playwright.Float(1000)})) +} diff --git a/tests/page_clock_test.go b/tests/page_clock_test.go index 417ce26c..1fa32616 100644 --- a/tests/page_clock_test.go +++ b/tests/page_clock_test.go @@ -600,7 +600,6 @@ func TestPageClockWhileRunning(t *testing.T) { require.NoError(t, page.Clock().PauseAt(1000)) //nolint:staticcheck page.WaitForTimeout(1000) - require.NoError(t, page.Clock().Resume()) now, err := page.Evaluate(`Date.now()`) require.NoError(t, err) require.LessOrEqual(t, 0, now) diff --git a/tests/route_test.go b/tests/route_test.go index b15bbf9c..56056386 100644 --- a/tests/route_test.go +++ b/tests/route_test.go @@ -2,6 +2,7 @@ package playwright_test import ( "encoding/json" + "fmt" "io" "net/http" "testing" @@ -324,3 +325,20 @@ func TestRequestTimingShouldWork(t *testing.T) { require.GreaterOrEqual(t, timing.ResponseEnd, timing.ResponseStart) require.Less(t, timing.ResponseEnd, 10000.0) } + +func TestShouldInterceptByGlob(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + require.NoError(t, page.Route("http://localhos**?*oo", func(route playwright.Route) { + require.NoError(t, route.Fulfill(playwright.RouteFulfillOptions{ + Body: "intercepted", + Status: playwright.Int(200), + })) + })) + + ret, err := page.Evaluate(`url => fetch(url).then(r => r.text())`, fmt.Sprintf("%s/?foo", server.CROSS_PROCESS_PREFIX)) + require.NoError(t, err) + require.Equal(t, "intercepted", ret) +}