Repository: grafana/k6

Issue:
Browser: `locator.pressSequentially`
# What

We want to implement `locator.pressSequentially` to allow k6 users to work with the `locator` API when filling input values on a page.

# Why

This is useful when the page expects keyboard input values down and up for each key, which `locator.fill` doesn't do.

This will help make migrating from PW to k6 a little easier. When working with PW's code gen tool in VSCode or working with MCP/AI, it will work with the latest PW APIs which k6 currently lacks. By implementing this, we will make the migration process simpler.

# How

Playwright API: https://playwright.dev/docs/api/class-locator#locator-press-sequentially
Playwright code: https://github.com/microsoft/playwright/blob/6626bba937af513cbcf05d5e164aa20b6d390ea2/packages/playwright-core/src/client/locator.ts#L350

Discussion:
@rajan2345: Hi ankur22! 👋

I'd like to work on implementing `locator.pressSequentially()` for this issue.

**What I've Done:**
- ✅ Studied the codebase and located the relevant files
- ✅ Reviewed Playwright's implementation
- ✅ Implemented `PressSequentially()` in `locator.go`
- ✅ Reused existing `FramePressOptions` (already has `delay` field)
- ✅ Added comprehensive test cases
- ✅ All tests passing locally

**Implementation Highlights:**
- Iterates through text character by character (handles Unicode correctly with runes)
- Calls existing `frame.press()` for each character
- Supports `delay` option for realistic typing simulation
- Follows the same pattern as other locator methods
- Includes timeout and strict mode tests

**Test Coverage:**
- Basic functionality (types text correctly)
- Delay option (timing verified)
- Works with textarea elements
- Timeout handling
- Strict mode enforcement

I'm ready to create a PR. Would you be open to assigning this to me?

Thanks! 

Solution patch:
diff --git a/internal/js/modules/k6/browser/browser/locator_mapping.go b/internal/js/modules/k6/browser/browser/locator_mapping.go
index 461e93dae..a30530333 100644
--- a/internal/js/modules/k6/browser/browser/locator_mapping.go
+++ b/internal/js/modules/k6/browser/browser/locator_mapping.go
@@ -355,6 +355,17 @@ func mapLocator(vu moduleVU, lo *common.Locator) mapping {
 				return nil, lo.Press(key, copts) //nolint:wrapcheck
 			}), nil
 		},
+
+		"pressSequentially": func(text string, opts sobek.Value) (*sobek.Promise, error) {
+			copts := common.NewFrameTypeOptions(lo.Timeout())
+			if err := copts.Parse(vu.Context(), opts); err != nil {
+				return nil, fmt.Errorf("parsing locator press sequentially options: %w", err)
+			}
+			return promise(vu, func() (any, error) {
+				return nil, lo.PressSequentially(text, copts) //nolint:wrapcheck
+			}), nil
+		},
+
 		"type": func(text string, opts sobek.Value) (*sobek.Promise, error) {
 			copts := common.NewFrameTypeOptions(lo.Timeout())
 			if err := copts.Parse(vu.Context(), opts); err != nil {
diff --git a/internal/js/modules/k6/browser/common/locator.go b/internal/js/modules/k6/browser/common/locator.go
index 85c2428d2..88466ff0c 100644
--- a/internal/js/modules/k6/browser/common/locator.go
+++ b/internal/js/modules/k6/browser/common/locator.go
@@ -532,6 +532,29 @@ func (l *Locator) Press(key string, opts *FramePressOptions) error {
 	return nil
 }
 
+// PressSequentially focuses on the element and sequentially sends a keydown,
+// keypress, and keyup events for each character in the provided string.
+// For handling special keys, use the [Locator.Press] method.
+func (l *Locator) PressSequentially(text string, opts *FrameTypeOptions) error {
+	l.log.Debugf(
+		"Locator:PressSequentially", "fid:%s furl:%q sel:%q text:%q opts:%+v",
+		l.frame.ID(), l.frame.URL(), l.selector, text, opts,
+	)
+	_, span := TraceAPICall(l.ctx, l.frame.page.targetID.String(), "locator.pressSequentially")
+	defer span.End()
+
+	opts.Strict = true
+	if err := l.frame.typ(l.selector, text, opts); err != nil {
+		err := fmt.Errorf("pressing sequentially %q on %q: %w", text, l.selector, err)
+		spanRecordError(span, err)
+		return err
+	}
+
+	applySlowMo(l.ctx)
+
+	return nil
+}
+
 // Type text on the element found that matches the locator's
 // selector with strict mode on.
 func (l *Locator) Type(text string, opts *FrameTypeOptions) error {
diff --git a/lib/execution.go b/lib/execution.go
index b42a70e5f..6132372ff 100644
--- a/lib/execution.go
+++ b/lib/execution.go
@@ -47,6 +47,7 @@ const (
 	ExecutionStatusTeardown
 	ExecutionStatusEnded
 	ExecutionStatusInterrupted
+	ExecutionStatusMarkedAsFailed
 )
 
 // ExecutionState contains a few different things:
diff --git a/lib/execution_status_gen.go b/lib/execution_status_gen.go
index e1a4dbeb5..ffdbdddb5 100644
--- a/lib/execution_status_gen.go
+++ b/lib/execution_status_gen.go
@@ -6,9 +6,9 @@ import (
 	"fmt"
 )
 
-const _ExecutionStatusName = "CreatedInitVUsInitExecutorsInitDonePausedBeforeRunStartedSetupRunningTeardownEndedInterrupted"
+const _ExecutionStatusName = "CreatedInitVUsInitExecutorsInitDonePausedBeforeRunStartedSetupRunningTeardownEndedInterruptedMarkedAsFailed"
 
-var _ExecutionStatusIndex = [...]uint8{0, 7, 14, 27, 35, 50, 57, 62, 69, 77, 82, 93}
+var _ExecutionStatusIndex = [...]uint8{0, 7, 14, 27, 35, 50, 57, 62, 69, 77, 82, 93, 107}
 
 func (i ExecutionStatus) String() string {
 	if i >= ExecutionStatus(len(_ExecutionStatusIndex)-1) {
@@ -17,20 +17,21 @@ func (i ExecutionStatus) String() string {
 	return _ExecutionStatusName[_ExecutionStatusIndex[i]:_ExecutionStatusIndex[i+1]]
 }
 
-var _ExecutionStatusValues = []ExecutionStatus{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
+var _ExecutionStatusValues = []ExecutionStatus{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
 
 var _ExecutionStatusNameToValueMap = map[string]ExecutionStatus{
-	_ExecutionStatusName[0:7]:   0,
-	_ExecutionStatusName[7:14]:  1,
-	_ExecutionStatusName[14:27]: 2,
-	_ExecutionStatusName[27:35]: 3,
-	_ExecutionStatusName[35:50]: 4,
-	_ExecutionStatusName[50:57]: 5,
-	_ExecutionStatusName[57:62]: 6,
-	_ExecutionStatusName[62:69]: 7,
-	_ExecutionStatusName[69:77]: 8,
-	_ExecutionStatusName[77:82]: 9,
-	_ExecutionStatusName[82:93]: 10,
+	_ExecutionStatusName[0:7]:    0,
+	_ExecutionStatusName[7:14]:   1,
+	_ExecutionStatusName[14:27]:  2,
+	_ExecutionStatusName[27:35]:  3,
+	_ExecutionStatusName[35:50]:  4,
+	_ExecutionStatusName[50:57]:  5,
+	_ExecutionStatusName[57:62]:  6,
+	_ExecutionStatusName[62:69]:  7,
+	_ExecutionStatusName[69:77]:  8,
+	_ExecutionStatusName[77:82]:  9,
+	_ExecutionStatusName[82:93]:  10,
+	_ExecutionStatusName[93:107]: 11,
 }
 
 // ExecutionStatusString retrieves an enum value from the enum constants string name.



Test patch:
diff --git a/internal/js/modules/k6/browser/browser/mapping_test.go b/internal/js/modules/k6/browser/browser/mapping_test.go
index f35a0a9d7..6c7a229b5 100644
--- a/internal/js/modules/k6/browser/browser/mapping_test.go
+++ b/internal/js/modules/k6/browser/browser/mapping_test.go
@@ -593,6 +593,7 @@ type locatorAPI interface { //nolint:interfacebloat
 	Nth(nth int) *common.Locator
 	SelectOption(values sobek.Value, opts sobek.Value) ([]string, error)
 	Press(key string, opts sobek.Value) error
+	PressSequentially(text string, opts sobek.Value) error
 	Type(text string, opts sobek.Value) error
 	Hover(opts sobek.Value) error
 	Tap(opts sobek.Value) error
diff --git a/internal/js/modules/k6/browser/tests/locator_test.go b/internal/js/modules/k6/browser/tests/locator_test.go
index 7fd168f79..e3a06c183 100644
--- a/internal/js/modules/k6/browser/tests/locator_test.go
+++ b/internal/js/modules/k6/browser/tests/locator_test.go
@@ -292,6 +292,46 @@ func TestLocator(t *testing.T) {
 				require.Equal(t, "xsomething", inputValue)
 			},
 		},
+
+		{
+			"PressSequentially", func(_ *testBrowser, p *common.Page) {
+				lo := p.Locator("#inputText", nil)
+				require.NoError(t, lo.Clear(common.NewFrameFillOptions(lo.Timeout())))
+
+				require.NoError(t, lo.PressSequentially("hello", common.NewFrameTypeOptions(lo.Timeout())))
+
+				value, err := p.InputValue("#inputText", common.NewFrameInputValueOptions(p.MainFrame().Timeout()))
+				require.NoError(t, err)
+				require.Equal(t, "hello", value)
+			},
+		},
+		{
+			"PressSequentiallyWithDelayOption", func(_ *testBrowser, p *common.Page) {
+				lo := p.Locator("#inputText", nil)
+				require.NoError(t, lo.Clear(common.NewFrameFillOptions(lo.Timeout())))
+
+				opts := common.NewFrameTypeOptions(lo.Timeout())
+				opts.Delay = 100
+
+				require.NoError(t, lo.PressSequentially("text", opts))
+
+				value, err := p.InputValue("#inputText", common.NewFrameInputValueOptions(p.MainFrame().Timeout()))
+				require.NoError(t, err)
+				require.Equal(t, "text", value)
+			},
+		},
+		{
+			"PressSequentiallyTextarea", func(_ *testBrowser, p *common.Page) {
+				lo := p.Locator("textarea", nil)
+				require.NoError(t, lo.Clear(common.NewFrameFillOptions(lo.Timeout())))
+
+				require.NoError(t, lo.PressSequentially("some text", common.NewFrameTypeOptions(lo.Timeout())))
+
+				value, err := lo.InputValue(common.NewFrameInputValueOptions(lo.Timeout()))
+				require.NoError(t, err)
+				require.Equal(t, "some text", value)
+			},
+		},
 		{
 			"SelectOption", func(tb *testBrowser, p *common.Page) {
 				l := p.Locator("#selectElement", nil)
@@ -467,6 +507,11 @@ func TestLocator(t *testing.T) {
 				return l.Press("a", common.NewFramePressOptions(100*time.Millisecond))
 			},
 		},
+		{
+			"PressSequentially", func(l *common.Locator, tb *testBrowser) error {
+				return l.PressSequentially("text", common.NewFrameTypeOptions(100*time.Millisecond))
+			},
+		},
 		{
 			"SetChecked", func(l *common.Locator, tb *testBrowser) error {
 				return l.SetChecked(true, common.NewFrameCheckOptions(100*time.Millisecond))