Skip to content

Commit c191518

Browse files
Merge branch 'master' into azure-trusted-signing
2 parents 55cf76b + c07f26a commit c191518

24 files changed

+1751
-410
lines changed

.github/workflows/build.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ jobs:
362362
with:
363363
persist-credentials: false
364364
- name: Install pandoc
365-
uses: crazy-max/ghaction-chocolatey@0e015857dd851f84fcb7fb53380eb5c4c8202333 # v3.0.0
365+
uses: crazy-max/ghaction-chocolatey@2526f467ccbd337d307fe179959cabbeca0bc8c0 # v3.4.0
366366
with:
367367
args: install -y pandoc
368368
- name: Install wix tools
@@ -371,7 +371,7 @@ jobs:
371371
Expand-Archive -Path .\wix311-binaries.zip -DestinationPath .\wix311\
372372
echo "$pwd\wix311" | Out-File -FilePath $env:GITHUB_PATH -Append
373373
- name: Download Windows binary
374-
uses: actions/download-artifact@v4
374+
uses: actions/download-artifact@v5
375375
with:
376376
name: ${{ needs.sign-binaries.outputs.windows_binary_artifact_name }}
377377
path: packaging
@@ -469,20 +469,20 @@ jobs:
469469
with:
470470
persist-credentials: false
471471
- name: Download binaries
472-
uses: actions/download-artifact@v4
472+
uses: actions/download-artifact@v5
473473
with:
474474
name: ${{ needs.sign-binaries.outputs.binary_artifact_name }}
475475
path: dist
476476
- name: Download Windows binaries
477-
uses: actions/download-artifact@v4
477+
uses: actions/download-artifact@v5
478478
with:
479479
name: ${{ needs.sign-packages.outputs.artifact_name }}
480480
path: dist
481481
- name: Generate checksum file
482482
run: cd dist && sha256sum * > "k6-${VERSION}-checksums.txt"
483483
- name: Anchore SBOM Action
484484
continue-on-error: true
485-
uses: anchore/sbom-action@7b36ad622f042cab6f59a75c2ac24ccb256e9b45 # v0.20.4
485+
uses: anchore/sbom-action@da167eac915b4e86f08b264dbdbc867b61be6f0c # v0.20.5
486486
with:
487487
artifact-name: k6-${{ env.VERSION }}-spdx.json
488488
upload-release-assets: false
@@ -515,12 +515,12 @@ jobs:
515515
with:
516516
persist-credentials: false
517517
- name: Download binaries
518-
uses: actions/download-artifact@v4
518+
uses: actions/download-artifact@v5
519519
with:
520520
name: ${{ needs.sign-binaries.outputs.binary_artifact_name }}
521521
path: dist
522522
- name: Download Windows binaries
523-
uses: actions/download-artifact@v4
523+
uses: actions/download-artifact@v5
524524
with:
525525
name: ${{ needs.sign-packages.outputs.artifact_name }}
526526
path: dist

examples/browser/locator_filter.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { browser } from 'k6/browser';
2+
import { check } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js';
3+
4+
export const options = {
5+
scenarios: {
6+
ui: {
7+
executor: 'shared-iterations',
8+
options: {
9+
browser: {
10+
type: 'chromium'
11+
}
12+
}
13+
}
14+
},
15+
thresholds: {
16+
checks: ["rate==1.0"]
17+
}
18+
}
19+
20+
export default async function() {
21+
const page = await browser.newPage();
22+
23+
await page.setContent(`
24+
<ul>
25+
<li>
26+
<h3>Product 1</h3>
27+
<button>Add to cart 1</button>
28+
</li>
29+
<li>
30+
<h3>Product 2</h3>
31+
<button>Add to cart 2</button>
32+
</li>
33+
</ul>
34+
`);
35+
36+
// Filter the 2nd item by filtering items with the "Product 2" text.
37+
await check(page, {
38+
'textContent': async p => {
39+
const txt = await p
40+
.getByRole('listitem')
41+
.filter({ hasText: 'Product 2' })
42+
.first()
43+
.textContent();
44+
return txt.includes(`Add to cart 2`);
45+
}
46+
});
47+
48+
// Using a regex, filter the 1st item by filtering items without the "Product 2" text.
49+
await check(page, {
50+
'textContent': async p => {
51+
const txt = await p
52+
.getByRole('listitem')
53+
.filter({ hasNotText: /Product 2/ })
54+
.first()
55+
.textContent();
56+
return txt.includes(`Add to cart 1`);
57+
}
58+
});
59+
60+
await page.close();
61+
}

internal/js/modules/k6/browser/browser/element_handle_mapping.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package browser
22

33
import (
4+
"errors"
45
"fmt"
56

67
"github.com/grafana/sobek"
@@ -15,7 +16,13 @@ func mapElementHandle(vu moduleVU, eh *common.ElementHandle) mapping { //nolint:
1516
maps := mapping{
1617
"boundingBox": func() *sobek.Promise {
1718
return k6ext.Promise(vu.Context(), func() (any, error) {
18-
return eh.BoundingBox(), nil
19+
box, err := eh.BoundingBox()
20+
// We want to avoid errors when an element is not visible and instead
21+
// opt to return a nil rectangle -- this matches Playwright's behaviour.
22+
if errors.Is(err, common.ErrElementNotVisible) {
23+
return nil, nil
24+
}
25+
return box, err
1926
})
2027
},
2128
"check": func(opts sobek.Value) (*sobek.Promise, error) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package browser
2+
3+
import (
4+
"github.com/grafana/sobek"
5+
"go.k6.io/k6/internal/js/modules/k6/browser/common"
6+
)
7+
8+
// mapFrameLocator API to the JS module.
9+
func mapFrameLocator(vu moduleVU, fl *common.FrameLocator) mapping {
10+
rt := vu.Runtime()
11+
return mapping{
12+
"locator": func(selector string) *sobek.Object {
13+
ml := mapLocator(vu, fl.Locator(selector))
14+
return rt.ToValue(ml).ToObject(rt)
15+
},
16+
}
17+
}

internal/js/modules/k6/browser/browser/frame_mapping.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping {
318318
}), nil
319319
},
320320
"locator": func(selector string, opts sobek.Value) mapping {
321-
return mapLocator(vu, f.Locator(selector, opts))
321+
return mapLocator(vu, f.Locator(selector, parseLocatorOptions(rt, opts)))
322322
},
323323
"name": f.Name,
324324
"page": func() mapping {
@@ -342,7 +342,7 @@ func mapFrame(vu moduleVU, f *common.Frame) mapping {
342342
return nil, fmt.Errorf("parsing select option options: %w", err)
343343
}
344344

345-
convValues, err := common.ConvertSelectOptionValues(vu.Runtime(), values)
345+
convValues, err := common.ConvertSelectOptionValues(rt, values)
346346
if err != nil {
347347
return nil, fmt.Errorf("parsing select options values: %w", err)
348348
}

internal/js/modules/k6/browser/browser/locator_mapping.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package browser
22

33
import (
4+
"errors"
45
"fmt"
56

67
"github.com/grafana/sobek"
78

89
"go.k6.io/k6/internal/js/modules/k6/browser/common"
910
"go.k6.io/k6/internal/js/modules/k6/browser/k6ext"
11+
k6common "go.k6.io/k6/js/common"
1012
)
1113

1214
// mapLocator API to the JS module.
@@ -57,6 +59,10 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
5759
return nil, lo.Click(popts) //nolint:wrapcheck
5860
}), nil
5961
},
62+
"contentFrame": func() *sobek.Object {
63+
ml := mapFrameLocator(vu, lo.ContentFrame())
64+
return rt.ToValue(ml).ToObject(rt)
65+
},
6066
"count": func() *sobek.Promise {
6167
return k6ext.Promise(vu.Context(), func() (any, error) {
6268
return lo.Count() //nolint:wrapcheck
@@ -117,6 +123,11 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
117123
return nil, lo.Fill(value, opts) //nolint:wrapcheck
118124
})
119125
},
126+
"filter": func(opts sobek.Value) mapping {
127+
return mapLocator(vu, lo.Filter(&common.LocatorFilterOptions{
128+
LocatorOptions: parseLocatorOptions(rt, opts),
129+
}))
130+
},
120131
"first": func() *sobek.Object {
121132
ml := mapLocator(vu, lo.First())
122133
return rt.ToValue(ml).ToObject(rt)
@@ -138,6 +149,69 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
138149
return s, nil
139150
})
140151
},
152+
"getByAltText": func(alt sobek.Value, opts sobek.Value) (*sobek.Object, error) {
153+
if k6common.IsNullish(alt) {
154+
return nil, errors.New("missing required argument 'altText'")
155+
}
156+
palt, popts := parseGetByBaseOptions(vu.Context(), alt, false, opts)
157+
158+
ml := mapLocator(vu, lo.GetByAltText(palt, popts))
159+
return rt.ToValue(ml).ToObject(rt), nil
160+
},
161+
"getByLabel": func(label sobek.Value, opts sobek.Value) (*sobek.Object, error) {
162+
if k6common.IsNullish(label) {
163+
return nil, errors.New("missing required argument 'label'")
164+
}
165+
plabel, popts := parseGetByBaseOptions(vu.Context(), label, true, opts)
166+
167+
ml := mapLocator(vu, lo.GetByLabel(plabel, popts))
168+
return rt.ToValue(ml).ToObject(rt), nil
169+
},
170+
"getByPlaceholder": func(placeholder sobek.Value, opts sobek.Value) (*sobek.Object, error) {
171+
if k6common.IsNullish(placeholder) {
172+
return nil, errors.New("missing required argument 'placeholder'")
173+
}
174+
pplaceholder, popts := parseGetByBaseOptions(vu.Context(), placeholder, false, opts)
175+
176+
ml := mapLocator(vu, lo.GetByPlaceholder(pplaceholder, popts))
177+
return rt.ToValue(ml).ToObject(rt), nil
178+
},
179+
"getByRole": func(role sobek.Value, opts sobek.Value) (*sobek.Object, error) {
180+
if k6common.IsNullish(role) {
181+
return nil, errors.New("missing required argument 'role'")
182+
}
183+
popts := parseGetByRoleOptions(vu.Context(), opts)
184+
185+
ml := mapLocator(vu, lo.GetByRole(role.String(), popts))
186+
return rt.ToValue(ml).ToObject(rt), nil
187+
},
188+
"getByTestId": func(testID sobek.Value) (*sobek.Object, error) {
189+
if k6common.IsNullish(testID) {
190+
return nil, errors.New("missing required argument 'testId'")
191+
}
192+
ptestID := parseStringOrRegex(testID, false)
193+
194+
ml := mapLocator(vu, lo.GetByTestID(ptestID))
195+
return rt.ToValue(ml).ToObject(rt), nil
196+
},
197+
"getByText": func(text sobek.Value, opts sobek.Value) (*sobek.Object, error) {
198+
if k6common.IsNullish(text) {
199+
return nil, errors.New("missing required argument 'text'")
200+
}
201+
ptext, popts := parseGetByBaseOptions(vu.Context(), text, true, opts)
202+
203+
ml := mapLocator(vu, lo.GetByText(ptext, popts))
204+
return rt.ToValue(ml).ToObject(rt), nil
205+
},
206+
"getByTitle": func(title sobek.Value, opts sobek.Value) (*sobek.Object, error) {
207+
if k6common.IsNullish(title) {
208+
return nil, errors.New("missing required argument 'title'")
209+
}
210+
ptitle, popts := parseGetByBaseOptions(vu.Context(), title, false, opts)
211+
212+
ml := mapLocator(vu, lo.GetByTitle(ptitle, popts))
213+
return rt.ToValue(ml).ToObject(rt), nil
214+
},
141215
"locator": func(selector string) *sobek.Object {
142216
ml := mapLocator(vu, lo.Locator(selector))
143217
return rt.ToValue(ml).ToObject(rt)
@@ -222,3 +296,23 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
222296
},
223297
}
224298
}
299+
300+
func parseLocatorOptions(rt *sobek.Runtime, opts sobek.Value) *common.LocatorOptions {
301+
if k6common.IsNullish(opts) {
302+
return nil
303+
}
304+
305+
var popts common.LocatorOptions
306+
307+
obj := opts.ToObject(rt)
308+
for _, k := range obj.Keys() {
309+
switch k {
310+
case "hasText":
311+
popts.HasText = obj.Get(k).String()
312+
case "hasNotText":
313+
popts.HasNotText = obj.Get(k).String()
314+
}
315+
}
316+
317+
return &popts
318+
}

internal/js/modules/k6/browser/browser/mapping_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ func TestMappings(t *testing.T) {
151151
return mapElementHandle(moduleVU{VU: vu}, &common.ElementHandle{})
152152
},
153153
},
154+
"frameLocator": {
155+
apiInterface: (*frameLocatorAPI)(nil),
156+
mapp: func() mapping {
157+
return mapFrameLocator(moduleVU{VU: vu}, &common.FrameLocator{})
158+
},
159+
},
154160
"jsHandle": {
155161
apiInterface: (*common.JSHandleAPI)(nil),
156162
mapp: func() mapping {
@@ -493,6 +499,10 @@ type elementHandleAPI interface { //nolint:interfacebloat
493499
WaitForSelector(selector string, opts sobek.Value) (*common.ElementHandle, error)
494500
}
495501

502+
type frameLocatorAPI interface {
503+
Locator(selector string) *common.Locator
504+
}
505+
496506
// requestAPI is the interface of an HTTP request.
497507
type requestAPI interface { //nolint:interfacebloat
498508
AllHeaders() map[string]string
@@ -538,6 +548,7 @@ type locatorAPI interface { //nolint:interfacebloat
538548
BoundingBox(opts *common.FrameBaseOptions) (*common.Rect, error)
539549
Clear(opts *common.FrameFillOptions) error
540550
Click(opts sobek.Value) error
551+
ContentFrame() *common.FrameLocator
541552
Count() (int, error)
542553
Dblclick(opts sobek.Value) error
543554
SetChecked(checked bool, opts sobek.Value) error
@@ -550,9 +561,17 @@ type locatorAPI interface { //nolint:interfacebloat
550561
IsVisible(opts sobek.Value) (bool, error)
551562
IsHidden(opts sobek.Value) (bool, error)
552563
Fill(value string, opts sobek.Value) error
564+
Filter(opts *common.LocatorFilterOptions) *common.Locator
553565
First() *common.Locator
554566
Focus(opts sobek.Value) error
555567
GetAttribute(name string, opts sobek.Value) (string, bool, error)
568+
GetByRole(role string, opts *common.GetByRoleOptions) *common.Locator
569+
GetByAltText(alt string, opts *common.GetByBaseOptions) *common.Locator
570+
GetByLabel(label string, opts *common.GetByBaseOptions) *common.Locator
571+
GetByPlaceholder(placeholder string, opts *common.GetByBaseOptions) *common.Locator
572+
GetByTitle(title string, opts *common.GetByBaseOptions) *common.Locator
573+
GetByTestId(testID string) *common.Locator
574+
GetByText(text string, opts *common.GetByBaseOptions) *common.Locator
556575
InnerHTML(opts sobek.Value) (string, error)
557576
InnerText(opts sobek.Value) (string, error)
558577
TextContent(opts sobek.Value) (string, bool, error)

internal/js/modules/k6/browser/browser/page_mapping.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ func mapPage(vu moduleVU, p *common.Page) mapping { //nolint:gocognit,cyclop
339339
},
340340
"keyboard": mapKeyboard(vu, p.GetKeyboard()),
341341
"locator": func(selector string, opts sobek.Value) *sobek.Object {
342-
ml := mapLocator(vu, p.Locator(selector, opts))
342+
ml := mapLocator(vu, p.Locator(selector, parseLocatorOptions(rt, opts)))
343343
return rt.ToValue(ml).ToObject(rt)
344344
},
345345
"mainFrame": func() *sobek.Object {
@@ -987,13 +987,7 @@ func waitForURLBodyImpl(vu moduleVU, target interface {
987987
return nil, fmt.Errorf("parsing waitForURL options: %w", err)
988988
}
989989

990-
var val string
991-
switch url.ExportType() {
992-
case reflect.TypeOf(string("")):
993-
val = fmt.Sprintf("'%s'", url.String()) // Strings require quotes
994-
default: // JS Regex, CSS, numbers or booleans
995-
val = url.String() // No quotes
996-
}
990+
val := parseStringOrRegex(url, false)
997991

998992
// Inject JS regex checker for URL pattern matching
999993
ctx, stopTaskqueue := context.WithCancel(vu.Context())

internal/js/modules/k6/browser/common/browser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ func (b *Browser) isAttachedPageValid(ev *target.EventAttachedToTarget, browserC
370370
}
371371
pageType := targetPage.Type
372372
if pageType != "page" && pageType != "background_page" {
373-
b.logger.Warnf(
373+
b.logger.Debugf(
374374
"Browser:isAttachedPageValid", "sid:%v tid:%v bctxid:%v bctx nil:%t, unknown target type: %q",
375375
ev.SessionID, targetPage.TargetID, targetPage.BrowserContextID, browserCtx == nil, targetPage.Type)
376376
return false

0 commit comments

Comments
 (0)