Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions internal/js/modules/k6/browser/browser/page_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -833,13 +833,15 @@ func parseWaitForFunctionArgs(
// so that it is easier to copy over updates/fixes from Playwright when we need
// to.
func parseStringOrRegex(v sobek.Value, doubleQuote bool) string {
const stringType = string("")

var a string
switch v.ExportType() {
case reflect.TypeOf(string("")): // text values require quotes
case reflect.TypeOf(stringType): // text values require quotes
if doubleQuote {
a = fmt.Sprintf(`"%s"`, v.String())
a = `"` + v.String() + `"`
} else {
a = fmt.Sprintf("'%s'", v.String())
a = `'` + v.String() + `'`
}
case reflect.TypeOf(map[string]interface{}(nil)): // JS RegExp
a = v.String() // No quotes
Expand Down
46 changes: 46 additions & 0 deletions internal/js/modules/k6/browser/browser/page_mapping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package browser

import (
"testing"

"github.com/grafana/sobek"
"github.com/stretchr/testify/require"
)

func TestParseStringOrRegex(t *testing.T) {
t.Parallel()

rt := sobek.New()
mk := func(code string) sobek.Value {
v, err := rt.RunString(code)
require.NoError(t, err)
return v
}

tests := []struct {
name string
input sobek.Value
doubleQuote bool
want string
}{
{name: "string_single_quote", input: mk(`'abc'`), doubleQuote: false, want: `'abc'`},
{name: "string_single_quote", input: mk(`'abc'`), doubleQuote: true, want: `"abc"`},
{name: "string_double_quote", input: mk(`"abc"`), doubleQuote: true, want: `"abc"`},
{name: "string_double_quote", input: mk(`"abc"`), doubleQuote: false, want: `'abc'`},
{name: "regex_literal", input: mk(`/ab+c/i`), doubleQuote: false, want: `/ab+c/i`},
{name: "number", input: mk(`123`), doubleQuote: true, want: `123`},
{name: "boolean", input: mk(`true`), doubleQuote: false, want: `true`},
{name: "object", input: mk(`({a:1})`), doubleQuote: false, want: `[object Object]`},
{name: "null", input: mk(`null`), doubleQuote: false, want: `null`},
{name: "undefined", input: mk(`undefined`), doubleQuote: false, want: `undefined`},
{name: "undefined", input: mk(``), doubleQuote: false, want: `undefined`},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := parseStringOrRegex(tc.input, tc.doubleQuote)
require.Equal(t, tc.want, got)
})
}
}
24 changes: 18 additions & 6 deletions internal/js/modules/k6/browser/common/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -1089,10 +1089,15 @@ func (f *Frame) GetByRole(role string, opts *GetByRoleOptions) *Locator {
// a string instead of as a regex in the getBy* APIs. It handles
// both single and double quotes.
func isQuotedText(s string) bool {
s = strings.TrimSpace(s)
if len(s) <= 1 {
return false
}

switch {
case len(s) > 0 && s[0] == '\'' && s[len(s)-1] == '\'':
case s[0] == '\'' && s[len(s)-1] == '\'':
return true
case len(s) > 0 && s[0] == '"' && s[len(s)-1] == '"':
case s[0] == '"' && s[len(s)-1] == '"':
return true
}
return false
Expand All @@ -1102,15 +1107,22 @@ func isQuotedText(s string) bool {
// for use with the internal:attr engine. It handles quoted strings and
// applies the appropriate suffix for exact or case-insensitive matching.
func (f *Frame) buildAttributeSelector(attrName, attrValue string, opts *GetByBaseOptions) string {
selector := "[" + attrName + "=" + attrValue + "]"
var b strings.Builder
// [, name, =, value, (i/s)?, ]
b.Grow(len(attrName) + len(attrValue) + 5)
b.WriteByte('[')
b.WriteString(attrName)
b.WriteByte('=')
b.WriteString(attrValue)
if isQuotedText(attrValue) {
if opts != nil && opts.Exact != nil && *opts.Exact {
selector = "[" + attrName + "=" + attrValue + "s]"
b.WriteByte('s')
} else {
selector = "[" + attrName + "=" + attrValue + "i]"
b.WriteByte('i')
}
}
return selector
b.WriteByte(']')
return b.String()
}

// Locator creates and returns a new locator for this frame.
Expand Down
119 changes: 119 additions & 0 deletions internal/js/modules/k6/browser/common/frame_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,122 @@ func (e *executionContextTestStub) eval( // this needs to be a pointer as otherw
) (res any, err error) {
return e.evalFn(apiCtx, opts, js, args...)
}

// toPtr is a helper function to convert a value to a pointer.
func toPtr[T any](v T) *T {
return &v
}

func TestBuildAttributeSelector(t *testing.T) {
t.Parallel()

f := &Frame{}

tests := []struct {
name string
attrName string
attrValue string
opts *GetByBaseOptions
want string
}{
{
name: "empty",
attrName: "",
attrValue: "",
opts: nil,
want: "[=]",
},
{
name: "unquoted_no_opts",
attrName: "data-test",
attrValue: "foo",
opts: nil,
want: "[data-test=foo]",
},
{
name: "quoted_single_nil_opts",
attrName: "data-test",
attrValue: "'Foo Bar'",
opts: nil,
want: "[data-test='Foo Bar'i]",
},
{
name: "quoted_single_exact_false",
attrName: "data-test",
attrValue: "'Foo Bar'",
opts: &GetByBaseOptions{Exact: toPtr(false)},
want: "[data-test='Foo Bar'i]",
},
{
name: "quoted_single_exact_true",
attrName: "data-test",
attrValue: "'Foo Bar'",
opts: &GetByBaseOptions{Exact: toPtr(true)},
want: "[data-test='Foo Bar's]",
},
{
name: "quoted_double_exact_true",
attrName: "data-test",
attrValue: "\"Foo Bar\"",
opts: &GetByBaseOptions{Exact: toPtr(true)},
want: "[data-test=\"Foo Bar\"s]",
},
{
name: "quoted_double_exact_false",
attrName: "data-test",
attrValue: "\"Foo Bar\"",
opts: &GetByBaseOptions{Exact: toPtr(false)},
want: "[data-test=\"Foo Bar\"i]",
},
{
name: "quoted_single_exact_nil",
attrName: "data-test",
attrValue: "'Foo Bar'",
opts: &GetByBaseOptions{Exact: nil},
want: "[data-test='Foo Bar'i]",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got := f.buildAttributeSelector(tc.attrName, tc.attrValue, tc.opts)
require.Equal(t, tc.want, got)
})
}
}

func TestIsQuotedText(t *testing.T) {
t.Parallel()

tests := []struct {
name string
in string
want bool
}{
{name: "empty", in: "", want: false},
{name: "unquoted", in: "foo", want: false},
{name: "single_quoted", in: "'foo'", want: true},
{name: "double_quoted", in: "\"foo\"", want: true},
{name: "mismatched_quotes_1", in: "'foo\"", want: false},
{name: "mismatched_quotes_2", in: "\"foo'", want: false},
{name: "just_single_quote", in: "'", want: false},
{name: "just_double_quote", in: "\"", want: false},
{name: "two_single_quotes", in: "''", want: true},
{name: "two_double_quotes", in: "\"\"", want: true},
{name: "leading_space_then_quoted", in: " 'foo'", want: true},
{name: "trailing_space_after_quoted", in: "'foo' ", want: true},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got := isQuotedText(tc.in)
if got != tc.want {
t.Fatalf("isQuotedText(%q) = %v, want %v", tc.in, got, tc.want)
}
})
}
}
Loading
Loading