diff --git a/cmd/getConfig.go b/cmd/getConfig.go
index 0712f22e8..f8f54af0e 100644
--- a/cmd/getConfig.go
+++ b/cmd/getConfig.go
@@ -138,6 +138,9 @@ func GetStageConfig() (config.StepConfig, error) {
 
 	defaultConfig := []io.ReadCloser{}
 	for _, f := range GeneralConfig.DefaultConfig {
+		if configOptions.OpenFile == nil {
+			return stepConfig, errors.New("config: open file function not set")
+		}
 		fc, err := configOptions.OpenFile(f, GeneralConfig.GitHubAccessTokens)
 		// only create error for non-default values
 		if err != nil && f != ".pipeline/defaults.yaml" {
diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go
index 56cebc07d..50b0d6dad 100644
--- a/cmd/metadata_generated.go
+++ b/cmd/metadata_generated.go
@@ -103,6 +103,7 @@ func GetAllStepMetadata() map[string]config.StepData {
 		"nexusUpload":                               nexusUploadMetadata(),
 		"npmExecuteLint":                            npmExecuteLintMetadata(),
 		"npmExecuteScripts":                         npmExecuteScriptsMetadata(),
+		"npmExecuteTests":                           npmExecuteTestsMetadata(),
 		"pipelineCreateScanSummary":                 pipelineCreateScanSummaryMetadata(),
 		"protecodeExecuteScan":                      protecodeExecuteScanMetadata(),
 		"pythonBuild":                               pythonBuildMetadata(),
diff --git a/cmd/mtaBuild_test.go b/cmd/mtaBuild_test.go
index 273ff3af9..e784fc1d5 100644
--- a/cmd/mtaBuild_test.go
+++ b/cmd/mtaBuild_test.go
@@ -87,6 +87,7 @@ func TestMtaBuild(t *testing.T) {
 	SetConfigOptions(ConfigCommandOptions{
 		OpenFile: config.OpenPiperFile,
 	})
+
 	t.Run("Application name not set", func(t *testing.T) {
 		utilsMock := newMtaBuildTestUtilsBundle()
 		options := mtaBuildOptions{}
diff --git a/cmd/npmExecuteTests.go b/cmd/npmExecuteTests.go
new file mode 100644
index 000000000..90dfaad2e
--- /dev/null
+++ b/cmd/npmExecuteTests.go
@@ -0,0 +1,115 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/SAP/jenkins-library/pkg/command"
+	"github.com/SAP/jenkins-library/pkg/log"
+	"github.com/SAP/jenkins-library/pkg/telemetry"
+)
+
+type vaultUrl struct {
+	URL      string `json:"url"`
+	Username string `json:"username,omitempty"`
+	Password string `json:"password,omitempty"`
+}
+
+func npmExecuteTests(config npmExecuteTestsOptions, _ *telemetry.CustomData) {
+	c := command.Command{}
+
+	c.Stdout(log.Writer())
+	c.Stderr(log.Writer())
+	err := runNpmExecuteTests(&config, &c)
+	if err != nil {
+		log.Entry().WithError(err).Fatal("Step execution failed")
+	}
+}
+
+func runNpmExecuteTests(config *npmExecuteTestsOptions, c command.ExecRunner) error {
+	if len(config.Envs) > 0 {
+		c.SetEnv(config.Envs)
+	}
+
+	if len(config.Paths) > 0 {
+		path := fmt.Sprintf("PATH=%s:%s", os.Getenv("PATH"), strings.Join(config.Paths, ":"))
+		c.SetEnv([]string{path})
+	}
+
+	if config.WorkingDirectory != "" {
+		if err := os.Chdir(config.WorkingDirectory); err != nil {
+			return fmt.Errorf("failed to change directory: %w", err)
+		}
+	}
+
+	installCommandTokens := strings.Fields(config.InstallCommand)
+	if err := c.RunExecutable(installCommandTokens[0], installCommandTokens[1:]...); err != nil {
+		return fmt.Errorf("failed to execute install command: %w", err)
+	}
+
+	parsedURLs, err := parseURLs(config.VaultURLs)
+	if err != nil {
+		return err
+	}
+
+	for _, app := range parsedURLs {
+		if err := runTestForUrl(app.URL, app.Username, app.Password, config, c); err != nil {
+			return err
+		}
+	}
+
+	if err := runTestForUrl(config.BaseURL, config.VaultUsername, config.VaultPassword, config, c); err != nil {
+		return err
+	}
+	return nil
+}
+
+func runTestForUrl(url, username, password string, config *npmExecuteTestsOptions, command command.ExecRunner) error {
+	credentialsToEnv(username, password, config.UsernameEnvVar, config.PasswordEnvVar, command)
+	// we need to reset the env vars as the next test might not have any credentials
+	defer resetCredentials(config.UsernameEnvVar, config.PasswordEnvVar, command)
+
+	runScriptTokens := strings.Fields(config.RunCommand)
+	if config.UrlOptionPrefix != "" {
+		runScriptTokens = append(runScriptTokens, config.UrlOptionPrefix+url)
+	}
+	if err := command.RunExecutable(runScriptTokens[0], runScriptTokens[1:]...); err != nil {
+		return fmt.Errorf("failed to execute npm script: %w", err)
+	}
+
+	return nil
+}
+
+func parseURLs(urls []map[string]interface{}) ([]vaultUrl, error) {
+	parsedUrls := []vaultUrl{}
+
+	for _, url := range urls {
+		parsedUrl := vaultUrl{}
+		urlStr, ok := url["url"].(string)
+		if !ok {
+			return nil, fmt.Errorf("url field is not a string")
+		}
+		parsedUrl.URL = urlStr
+		if username, ok := url["username"].(string); ok {
+			parsedUrl.Username = username
+		}
+
+		if password, ok := url["password"].(string); ok {
+			parsedUrl.Password = password
+		}
+		parsedUrls = append(parsedUrls, parsedUrl)
+	}
+	return parsedUrls, nil
+}
+
+func credentialsToEnv(username, password, usernameEnv, passwordEnv string, c command.ExecRunner) {
+	if username == "" || password == "" {
+		return
+	}
+	c.SetEnv([]string{usernameEnv + "=" + username, passwordEnv + "=" + password})
+}
+
+func resetCredentials(usernameEnv, passwordEnv string, c command.ExecRunner) {
+	c.SetEnv([]string{usernameEnv + "=", passwordEnv + "="})
+}
diff --git a/cmd/npmExecuteTests_generated.go b/cmd/npmExecuteTests_generated.go
new file mode 100644
index 000000000..3d794ea32
--- /dev/null
+++ b/cmd/npmExecuteTests_generated.go
@@ -0,0 +1,376 @@
+// Code generated by piper's step-generator. DO NOT EDIT.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"reflect"
+	"strings"
+	"time"
+
+	"github.com/SAP/jenkins-library/pkg/config"
+	"github.com/SAP/jenkins-library/pkg/gcp"
+	"github.com/SAP/jenkins-library/pkg/gcs"
+	"github.com/SAP/jenkins-library/pkg/log"
+	"github.com/SAP/jenkins-library/pkg/splunk"
+	"github.com/SAP/jenkins-library/pkg/telemetry"
+	"github.com/SAP/jenkins-library/pkg/validation"
+	"github.com/bmatcuk/doublestar"
+	"github.com/spf13/cobra"
+)
+
+type npmExecuteTestsOptions struct {
+	InstallCommand   string                   `json:"installCommand,omitempty"`
+	RunCommand       string                   `json:"runCommand,omitempty"`
+	VaultURLs        []map[string]interface{} `json:"vaultURLs,omitempty"`
+	VaultUsername    string                   `json:"vaultUsername,omitempty"`
+	VaultPassword    string                   `json:"vaultPassword,omitempty"`
+	BaseURL          string                   `json:"baseUrl,omitempty"`
+	UsernameEnvVar   string                   `json:"usernameEnvVar,omitempty"`
+	PasswordEnvVar   string                   `json:"passwordEnvVar,omitempty"`
+	UrlOptionPrefix  string                   `json:"urlOptionPrefix,omitempty"`
+	Envs             []string                 `json:"envs,omitempty"`
+	Paths            []string                 `json:"paths,omitempty"`
+	WorkingDirectory string                   `json:"workingDirectory,omitempty"`
+}
+
+type npmExecuteTestsReports struct {
+}
+
+func (p *npmExecuteTestsReports) persist(stepConfig npmExecuteTestsOptions, gcpJsonKeyFilePath string, gcsBucketId string, gcsFolderPath string, gcsSubFolder string) {
+	if gcsBucketId == "" {
+		log.Entry().Info("persisting reports to GCS is disabled, because gcsBucketId is empty")
+		return
+	}
+	log.Entry().Info("Uploading reports to Google Cloud Storage...")
+	content := []gcs.ReportOutputParam{
+		{FilePattern: "**/e2e-results.xml", ParamRef: "", StepResultType: "end-to-end-test"},
+	}
+	envVars := []gcs.EnvVar{
+		{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: gcpJsonKeyFilePath, Modified: false},
+	}
+	gcsClient, err := gcs.NewClient(gcs.WithEnvVars(envVars))
+	if err != nil {
+		log.Entry().Errorf("creation of GCS client failed: %v", err)
+		return
+	}
+	defer gcsClient.Close()
+	structVal := reflect.ValueOf(&stepConfig).Elem()
+	inputParameters := map[string]string{}
+	for i := 0; i < structVal.NumField(); i++ {
+		field := structVal.Type().Field(i)
+		if field.Type.String() == "string" {
+			paramName := strings.Split(field.Tag.Get("json"), ",")
+			paramValue, _ := structVal.Field(i).Interface().(string)
+			inputParameters[paramName[0]] = paramValue
+		}
+	}
+	if err := gcs.PersistReportsToGCS(gcsClient, content, inputParameters, gcsFolderPath, gcsBucketId, gcsSubFolder, doublestar.Glob, os.Stat); err != nil {
+		log.Entry().Errorf("failed to persist reports: %v", err)
+	}
+}
+
+// NpmExecuteTestsCommand Executes end-to-end tests using npm
+func NpmExecuteTestsCommand() *cobra.Command {
+	const STEP_NAME = "npmExecuteTests"
+
+	metadata := npmExecuteTestsMetadata()
+	var stepConfig npmExecuteTestsOptions
+	var startTime time.Time
+	var reports npmExecuteTestsReports
+	var logCollector *log.CollectorHook
+	var splunkClient *splunk.Splunk
+	telemetryClient := &telemetry.Telemetry{}
+
+	var createNpmExecuteTestsCmd = &cobra.Command{
+		Use:   STEP_NAME,
+		Short: "Executes end-to-end tests using npm",
+		Long: `This step executes end-to-end tests in a Docker environment using npm.
+
+The step spins up a Docker container based on the specified ` + "`" + `dockerImage` + "`" + ` and executes the ` + "`" + `installScript` + "`" + ` and ` + "`" + `runScript` + "`" + ` from ` + "`" + `package.json` + "`" + `.
+
+The application URLs and credentials can be specified in ` + "`" + `appUrls` + "`" + ` and ` + "`" + `credentialsId` + "`" + ` respectively. If ` + "`" + `wdi5` + "`" + ` is set to ` + "`" + `true` + "`" + `, the step uses ` + "`" + `wdi5_username` + "`" + ` and ` + "`" + `wdi5_password` + "`" + ` for authentication.
+
+The tests can be restricted to run only on the productive branch by setting ` + "`" + `onlyRunInProductiveBranch` + "`" + ` to ` + "`" + `true` + "`" + `.`,
+		PreRunE: func(cmd *cobra.Command, _ []string) error {
+			startTime = time.Now()
+			log.SetStepName(STEP_NAME)
+			log.SetVerbose(GeneralConfig.Verbose)
+
+			GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens)
+
+			path, err := os.Getwd()
+			if err != nil {
+				return err
+			}
+			fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
+			log.RegisterHook(fatalHook)
+
+			err = PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile)
+			if err != nil {
+				log.SetErrorCategory(log.ErrorConfiguration)
+				return err
+			}
+
+			if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
+				sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
+				log.RegisterHook(&sentryHook)
+			}
+
+			if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 || len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
+				splunkClient = &splunk.Splunk{}
+				logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID}
+				log.RegisterHook(logCollector)
+			}
+
+			if err = log.RegisterANSHookIfConfigured(GeneralConfig.CorrelationID); err != nil {
+				log.Entry().WithError(err).Warn("failed to set up SAP Alert Notification Service log hook")
+			}
+
+			validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages())
+			if err != nil {
+				return err
+			}
+			if err = validation.ValidateStruct(stepConfig); err != nil {
+				log.SetErrorCategory(log.ErrorConfiguration)
+				return err
+			}
+
+			return nil
+		},
+		Run: func(_ *cobra.Command, _ []string) {
+			vaultClient := config.GlobalVaultClient()
+			if vaultClient != nil {
+				defer vaultClient.MustRevokeToken()
+			}
+
+			stepTelemetryData := telemetry.CustomData{}
+			stepTelemetryData.ErrorCode = "1"
+			handler := func() {
+				reports.persist(stepConfig, GeneralConfig.GCPJsonKeyFilePath, GeneralConfig.GCSBucketId, GeneralConfig.GCSFolderPath, GeneralConfig.GCSSubFolder)
+				config.RemoveVaultSecretFiles()
+				stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds())
+				stepTelemetryData.ErrorCategory = log.GetErrorCategory().String()
+				stepTelemetryData.PiperCommitHash = GitCommit
+				telemetryClient.SetData(&stepTelemetryData)
+				telemetryClient.Send()
+				if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 {
+					splunkClient.Initialize(GeneralConfig.CorrelationID,
+						GeneralConfig.HookConfig.SplunkConfig.Dsn,
+						GeneralConfig.HookConfig.SplunkConfig.Token,
+						GeneralConfig.HookConfig.SplunkConfig.Index,
+						GeneralConfig.HookConfig.SplunkConfig.SendLogs)
+					splunkClient.Send(telemetryClient.GetData(), logCollector)
+				}
+				if len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 {
+					splunkClient.Initialize(GeneralConfig.CorrelationID,
+						GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint,
+						GeneralConfig.HookConfig.SplunkConfig.ProdCriblToken,
+						GeneralConfig.HookConfig.SplunkConfig.ProdCriblIndex,
+						GeneralConfig.HookConfig.SplunkConfig.SendLogs)
+					splunkClient.Send(telemetryClient.GetData(), logCollector)
+				}
+				if GeneralConfig.HookConfig.GCPPubSubConfig.Enabled {
+					err := gcp.NewGcpPubsubClient(
+						vaultClient,
+						GeneralConfig.HookConfig.GCPPubSubConfig.ProjectNumber,
+						GeneralConfig.HookConfig.GCPPubSubConfig.IdentityPool,
+						GeneralConfig.HookConfig.GCPPubSubConfig.IdentityProvider,
+						GeneralConfig.CorrelationID,
+						GeneralConfig.HookConfig.OIDCConfig.RoleID,
+					).Publish(GeneralConfig.HookConfig.GCPPubSubConfig.Topic, telemetryClient.GetDataBytes())
+					if err != nil {
+						log.Entry().WithError(err).Warn("event publish failed")
+					}
+				}
+			}
+			log.DeferExitHandler(handler)
+			defer handler()
+			telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME, GeneralConfig.HookConfig.PendoConfig.Token)
+			npmExecuteTests(stepConfig, &stepTelemetryData)
+			stepTelemetryData.ErrorCode = "0"
+			log.Entry().Info("SUCCESS")
+		},
+	}
+
+	addNpmExecuteTestsFlags(createNpmExecuteTestsCmd, &stepConfig)
+	return createNpmExecuteTestsCmd
+}
+
+func addNpmExecuteTestsFlags(cmd *cobra.Command, stepConfig *npmExecuteTestsOptions) {
+	cmd.Flags().StringVar(&stepConfig.InstallCommand, "installCommand", `npm ci`, "Command to be executed for installation`.")
+	cmd.Flags().StringVar(&stepConfig.RunCommand, "runCommand", `npm run wdi5`, "Command to be executed for running tests`.")
+
+	cmd.Flags().StringVar(&stepConfig.VaultUsername, "vaultUsername", os.Getenv("PIPER_vaultUsername"), "The base URL username.")
+	cmd.Flags().StringVar(&stepConfig.VaultPassword, "vaultPassword", os.Getenv("PIPER_vaultPassword"), "The base URL password.")
+	cmd.Flags().StringVar(&stepConfig.BaseURL, "baseUrl", `http://localhost:8080/index.html`, "Base URL of the application to be tested.")
+	cmd.Flags().StringVar(&stepConfig.UsernameEnvVar, "usernameEnvVar", `wdi5_username`, "Env var for username.")
+	cmd.Flags().StringVar(&stepConfig.PasswordEnvVar, "passwordEnvVar", `wdi5_password`, "Env var for password.")
+	cmd.Flags().StringVar(&stepConfig.UrlOptionPrefix, "urlOptionPrefix", os.Getenv("PIPER_urlOptionPrefix"), "If you want to specify an extra option that the tested url it appended to.\nFor example if the test URL is `http://localhost and urlOptionPrefix is `--base-url=`,\nwe'll add `--base-url=http://localhost` to your runScript.\n")
+	cmd.Flags().StringSliceVar(&stepConfig.Envs, "envs", []string{}, "List of environment variables to be set")
+	cmd.Flags().StringSliceVar(&stepConfig.Paths, "paths", []string{}, "List of paths to be added to $PATH")
+	cmd.Flags().StringVar(&stepConfig.WorkingDirectory, "workingDirectory", `.`, "Directory where your tests are located relative to the root of your project")
+
+	cmd.MarkFlagRequired("runCommand")
+}
+
+// retrieve step metadata
+func npmExecuteTestsMetadata() config.StepData {
+	var theMetaData = config.StepData{
+		Metadata: config.StepMetadata{
+			Name:        "npmExecuteTests",
+			Aliases:     []config.Alias{},
+			Description: "Executes end-to-end tests using npm",
+		},
+		Spec: config.StepSpec{
+			Inputs: config.StepInputs{
+				Parameters: []config.StepParameters{
+					{
+						Name:        "installCommand",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     `npm ci`,
+					},
+					{
+						Name:        "runCommand",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   true,
+						Aliases:     []config.Alias{},
+						Default:     `npm run wdi5`,
+					},
+					{
+						Name: "vaultURLs",
+						ResourceRef: []config.ResourceReference{
+							{
+								Name:    "appMetadataVaultSecretName",
+								Type:    "vaultSecret",
+								Default: "appMetadata",
+							},
+						},
+						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:      "[]map[string]interface{}",
+						Mandatory: false,
+						Aliases:   []config.Alias{},
+					},
+					{
+						Name: "vaultUsername",
+						ResourceRef: []config.ResourceReference{
+							{
+								Name:    "appMetadataVaultSecretName",
+								Type:    "vaultSecret",
+								Default: "appMetadata",
+							},
+						},
+						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:      "string",
+						Mandatory: false,
+						Aliases:   []config.Alias{},
+						Default:   os.Getenv("PIPER_vaultUsername"),
+					},
+					{
+						Name: "vaultPassword",
+						ResourceRef: []config.ResourceReference{
+							{
+								Name:    "appMetadataVaultSecretName",
+								Type:    "vaultSecret",
+								Default: "appMetadata",
+							},
+						},
+						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:      "string",
+						Mandatory: false,
+						Aliases:   []config.Alias{},
+						Default:   os.Getenv("PIPER_vaultPassword"),
+					},
+					{
+						Name:        "baseUrl",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     `http://localhost:8080/index.html`,
+					},
+					{
+						Name:        "usernameEnvVar",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     `wdi5_username`,
+					},
+					{
+						Name:        "passwordEnvVar",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     `wdi5_password`,
+					},
+					{
+						Name:        "urlOptionPrefix",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     os.Getenv("PIPER_urlOptionPrefix"),
+					},
+					{
+						Name:        "envs",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "[]string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     []string{},
+					},
+					{
+						Name:        "paths",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "[]string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     []string{},
+					},
+					{
+						Name:        "workingDirectory",
+						ResourceRef: []config.ResourceReference{},
+						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"},
+						Type:        "string",
+						Mandatory:   false,
+						Aliases:     []config.Alias{},
+						Default:     `.`,
+					},
+				},
+			},
+			Containers: []config.Container{
+				{Name: "node", Image: "node:lts-bookworm", EnvVars: []config.EnvVar{{Name: "BASE_URL", Value: "${{params.baseUrl}}"}, {Name: "CREDENTIALS_ID", Value: "${{params.credentialsId}}"}, {Name: "no_proxy", Value: "localhost,selenium,$no_proxy"}, {Name: "NO_PROXY", Value: "localhost,selenium,$NO_PROXY"}}, WorkingDir: "/home/node"},
+			},
+			Sidecars: []config.Container{
+				{Name: "selenium", Image: "selenium/standalone-chrome", EnvVars: []config.EnvVar{{Name: "NO_PROXY", Value: "localhost,selenium,$NO_PROXY"}, {Name: "no_proxy", Value: "localhost,selenium,$no_proxy"}}},
+			},
+			Outputs: config.StepOutputs{
+				Resources: []config.StepResources{
+					{
+						Name: "reports",
+						Type: "reports",
+						Parameters: []map[string]interface{}{
+							{"filePattern": "**/e2e-results.xml", "type": "end-to-end-test"},
+						},
+					},
+				},
+			},
+		},
+	}
+	return theMetaData
+}
diff --git a/cmd/npmExecuteTests_generated_test.go b/cmd/npmExecuteTests_generated_test.go
new file mode 100644
index 000000000..58bab7ed1
--- /dev/null
+++ b/cmd/npmExecuteTests_generated_test.go
@@ -0,0 +1,20 @@
+//go:build unit
+// +build unit
+
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNpmExecuteTestsCommand(t *testing.T) {
+	t.Parallel()
+
+	testCmd := NpmExecuteTestsCommand()
+
+	// only high level testing performed - details are tested in step generation procedure
+	assert.Equal(t, "npmExecuteTests", testCmd.Use, "command name incorrect")
+
+}
diff --git a/cmd/npmExecuteTests_test.go b/cmd/npmExecuteTests_test.go
new file mode 100644
index 000000000..bd044ead6
--- /dev/null
+++ b/cmd/npmExecuteTests_test.go
@@ -0,0 +1,88 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRunNpmExecuteTests(t *testing.T) {
+	t.Parallel()
+
+	testCmd := NpmExecuteTestsCommand()
+
+	// only high level testing performed - details are tested in step generation procedure
+	assert.Equal(t, "npmExecuteTests", testCmd.Use, "command name incorrect")
+}
+
+func TestParseURLs(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []map[string]interface{}
+		expected []vaultUrl
+		wantErr  bool
+	}{
+		{
+			name: "Valid URLs",
+			input: []map[string]interface{}{
+				{
+					"url":      "http://example.com",
+					"username": "user1",
+					"password": "pass1",
+				},
+				{
+					"url": "http://example2.com",
+				},
+			},
+			expected: []vaultUrl{
+				{
+					URL:      "http://example.com",
+					Username: "user1",
+					Password: "pass1",
+				},
+				{
+					URL: "http://example2.com",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Invalid URL entry",
+			input: []map[string]interface{}{
+				{
+					"username": "user1",
+				},
+			},
+			expected: nil,
+			wantErr:  true,
+		},
+		{
+			name: "Invalid URL field type",
+			input: []map[string]interface{}{
+				{
+					"url": 123,
+				},
+			},
+			expected: nil,
+			wantErr:  true,
+		},
+		{
+			name:     "Empty URLs",
+			input:    []map[string]interface{}{},
+			expected: []vaultUrl{},
+			wantErr:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := parseURLs(tt.input)
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.expected, got)
+		})
+	}
+}
diff --git a/cmd/piper.go b/cmd/piper.go
index 1b4972987..295827f8f 100644
--- a/cmd/piper.go
+++ b/cmd/piper.go
@@ -24,14 +24,14 @@ type GeneralConfigOptions struct {
 	CorrelationID        string
 	CustomConfig         string
 	GitHubTokens         []string // list of entries in form of <server>:<token> to allow token authentication for downloading config / defaults
-	DefaultConfig        []string //ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
+	DefaultConfig        []string // ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
 	IgnoreCustomDefaults bool
 	ParametersJSON       string
 	EnvRootPath          string
 	NoTelemetry          bool
 	StageName            string
 	StepConfigJSON       string
-	StepMetadata         string //metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
+	StepMetadata         string // metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
 	StepName             string
 	Verbose              bool
 	LogFormat            string
@@ -161,6 +161,7 @@ func Execute() {
 	rootCmd.AddCommand(AbapEnvironmentRunATCCheckCommand())
 	rootCmd.AddCommand(NpmExecuteScriptsCommand())
 	rootCmd.AddCommand(NpmExecuteLintCommand())
+	rootCmd.AddCommand(NpmExecuteTestsCommand())
 	rootCmd.AddCommand(GctsCreateRepositoryCommand())
 	rootCmd.AddCommand(GctsExecuteABAPQualityChecksCommand())
 	rootCmd.AddCommand(GctsExecuteABAPUnitTestsCommand())
@@ -269,7 +270,6 @@ func addRootFlags(rootCmd *cobra.Command) {
 	rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSFolderPath, "gcsFolderPath", "", "GCS folder path. One of the components of GCS target folder")
 	rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSBucketId, "gcsBucketId", "", "Bucket name for Google Cloud Storage")
 	rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSSubFolder, "gcsSubFolder", "", "Used to logically separate results of the same step result type")
-
 }
 
 // ResolveAccessTokens reads a list of tokens in format host:token passed via command line
@@ -353,7 +353,6 @@ func initStageName(outputToLog bool) {
 
 // PrepareConfig reads step configuration from various sources and merges it (defaults, config file, flags, ...)
 func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName string, options interface{}, openFile func(s string, t map[string]string) (io.ReadCloser, error)) error {
-
 	log.SetFormatter(GeneralConfig.LogFormat)
 
 	initStageName(true)
@@ -398,7 +397,7 @@ func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName strin
 		// use config & defaults
 		var customConfig io.ReadCloser
 		var err error
-		//accept that config file and defaults cannot be loaded since both are not mandatory here
+		// accept that config file and defaults cannot be loaded since both are not mandatory here
 		{
 			projectConfigFile := getProjectConfigFile(GeneralConfig.CustomConfig)
 			if exists, err := piperutils.FileExists(projectConfigFile); exists {
@@ -625,7 +624,6 @@ func getStepOptionsStructType(stepOptions interface{}) reflect.Type {
 }
 
 func getProjectConfigFile(name string) string {
-
 	var altName string
 	if ext := filepath.Ext(name); ext == ".yml" {
 		altName = fmt.Sprintf("%v.yaml", strings.TrimSuffix(name, ext))
diff --git a/documentation/docs/steps/npmExecuteTests.md b/documentation/docs/steps/npmExecuteTests.md
new file mode 100644
index 000000000..26af47289
--- /dev/null
+++ b/documentation/docs/steps/npmExecuteTests.md
@@ -0,0 +1,114 @@
+# ${docGenStepName} (Beta)
+
+[!WARNING]
+Please note, that the npmExecuteTests step is in beta state, and there could be breaking changes before we remove the beta notice.
+
+## ${docGenDescription}
+
+## ${docGenParameters}
+
+## ${docGenConfiguration}
+
+## Examples
+
+### Simple example using wdi5
+
+```yaml
+stages:
+  - name: Test
+    steps:
+      - name: npmExecuteTests
+        type: npmExecuteTests
+        params:
+          baseUrl: "http://example.com/index.html"
+```
+
+This will run your wdi5 tests with the given baseUrl.
+
+### Advanced example using custom test script with credentials using Vault
+
+```yaml
+stages:
+  - name: Test
+    steps:
+      - name: npmExecuteTests
+        type: npmExecuteTests
+        params:
+          installCommand: "npm install"
+          runCommand: "npm run custom-e2e-test"
+          usernameEnvVar: "e2e_username"
+          passwordEnvVar: "e2e_password"
+          baseUrl: "http://example.com/index.html"
+          urlOptionPrefix: "--base-url="
+```
+
+and Vault configuration in PIPELINE-GROUP-<id>/PIPELINE-<id>/appMetadata
+
+```json
+{
+  "vaultURLs": [
+    {
+      "url": "http://one.example.com/index.html",
+      "username": "some-username1",
+      "password": "some-password1"
+    },
+    {
+      "url": "http://two.example.com/index.html",
+      "username": "some-username2",
+      "password": "some-password2"
+    }
+  ],
+  "vaultUsername": "base-url-username",
+  "vaultPassword": "base-url-password"
+}
+```
+
+This will run your custom install and run script for each URL from secrets and use the given URL like so:
+
+```shell
+npm run custom-e2e-test --base-url=http://one.example.com/index.html
+```
+
+Each test run will have their own environment variables set:
+
+```shell
+e2e_username=some-username1
+e2e_password=some-password1
+```
+
+Environment variables are reset before each test run with their corresponding values from the secrets
+
+### Custom environment variables and $PATH
+
+```yaml
+stages:
+  - name: Test
+    steps:
+      - name: npmExecuteTests
+        type: npmExecuteTests
+        params:
+          envs:
+            - "MY_ENV_VAR=value"
+          paths:
+            - "/path/to/add"
+```
+
+If you're running uiVeri5 tests, you might need to set additional environment variables or add paths to the $PATH variable. This can be done using the `envs` and `paths` parameters:
+
+```yaml
+stages:
+  - name: Test
+    steps:
+      - name: npmExecuteTests
+        type: npmExecuteTests
+        params:
+          runCommand: "/home/node/.npm-global/bin/uiveri5"
+          installCommand: "npm install @ui5/uiveri5 --global --quiet"
+          runOptions: ["--seleniumAddress=http://localhost:4444/wd/hub"]
+          usernameEnvVar: "PIPER_SELENIUM_HUB_USER"
+          passwordEnvVar: "PIPER_SELENIUM_HUB_PASSWORD"
+          envs:
+            - "NPM_CONFIG_PREFIX=~/.npm-global"
+          paths:
+            - "~/.npm-global/bin"
+```
diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml
index c7e3674b7..953bfb2c9 100644
--- a/documentation/mkdocs.yml
+++ b/documentation/mkdocs.yml
@@ -155,6 +155,7 @@ nav:
         - npmExecuteEndToEndTests: steps/npmExecuteEndToEndTests.md
         - npmExecuteLint: steps/npmExecuteLint.md
         - npmExecuteScripts: steps/npmExecuteScripts.md
+        - npmExecuteTests: steps/npmExecuteTests.md
         - pipelineExecute: steps/pipelineExecute.md
         - pipelineRestartSteps: steps/pipelineRestartSteps.md
         - pipelineStashFiles: steps/pipelineStashFiles.md
diff --git a/resources/metadata/npmExecuteTests.yaml b/resources/metadata/npmExecuteTests.yaml
new file mode 100644
index 000000000..7eb153042
--- /dev/null
+++ b/resources/metadata/npmExecuteTests.yaml
@@ -0,0 +1,159 @@
+metadata:
+  name: npmExecuteTests
+  description: Executes end-to-end tests using npm
+  longDescription: |
+    This step executes end-to-end tests in a Docker environment using npm.
+
+    The step spins up a Docker container based on the specified `dockerImage` and executes the `installScript` and `runScript` from `package.json`.
+
+    The application URLs and credentials can be specified in `appUrls` and `credentialsId` respectively. If `wdi5` is set to `true`, the step uses `wdi5_username` and `wdi5_password` for authentication.
+
+    The tests can be restricted to run only on the productive branch by setting `onlyRunInProductiveBranch` to `true`.
+
+spec:
+  inputs:
+    params:
+      - name: installCommand
+        type: string
+        description: Command to be executed for installation`.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+        default: "npm ci"
+      - name: runCommand
+        type: string
+        description: Command to be executed for running tests`.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+        mandatory: true
+        default: "npm run wdi5"
+      - name: vaultURLs
+        type: "[]map[string]interface{}"
+        description: |
+          An array of objects, each representing an application URL with associated credentials.
+          Each object must have the following properties:
+          - `url`: The URL of the application.
+          - `username`: The username for accessing the application.
+          - `password`: The password for accessing the application.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+        resourceRef:
+          - type: vaultSecret
+            default: appMetadata
+            name: appMetadataVaultSecretName
+      - name: vaultUsername
+        type: "string"
+        description: The base URL username.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+        resourceRef:
+          - type: vaultSecret
+            default: appMetadata
+            name: appMetadataVaultSecretName
+      - name: vaultPassword
+        type: "string"
+        description: The base URL password.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+        resourceRef:
+          - type: vaultSecret
+            default: appMetadata
+            name: appMetadataVaultSecretName
+      - name: baseUrl
+        type: string
+        default: "http://localhost:8080/index.html"
+        description: Base URL of the application to be tested.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+      - name: usernameEnvVar
+        type: string
+        default: "wdi5_username"
+        description: Env var for username.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+      - name: passwordEnvVar
+        type: string
+        default: "wdi5_password"
+        description: Env var for password.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+      - name: urlOptionPrefix
+        type: string
+        description: |
+          If you want to specify an extra option that the tested url it appended to.
+          For example if the test URL is `http://localhost and urlOptionPrefix is `--base-url=`,
+          we'll add `--base-url=http://localhost` to your runScript.
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+      - name: envs
+        type: "[]string"
+        description: List of environment variables to be set
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+      - name: paths
+        type: "[]string"
+        description: List of paths to be added to $PATH
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+      - name: workingDirectory
+        type: string
+        default: "."
+        description: Directory where your tests are located relative to the root of your project
+        scope:
+          - PARAMETERS
+          - STAGES
+          - STEPS
+  outputs:
+    resources:
+      - name: reports
+        type: reports
+        params:
+          - filePattern: "**/e2e-results.xml"
+            type: end-to-end-test
+  containers:
+    - name: node
+      image: node:lts-bookworm
+      env:
+        - name: BASE_URL
+          value: ${{params.baseUrl}}
+        - name: CREDENTIALS_ID
+          value: ${{params.credentialsId}}
+        - name: no_proxy
+          value: localhost,selenium,$no_proxy
+        - name: NO_PROXY
+          value: localhost,selenium,$NO_PROXY
+      workingDir: /home/node
+  sidecars:
+    - image: selenium/standalone-chrome
+      name: selenium
+      securityContext:
+        privileged: true
+      volumeMounts:
+        - mountPath: /dev/shm
+          name: dev-shm
+      env:
+        - name: "NO_PROXY"
+          value: "localhost,selenium,$NO_PROXY"
+        - name: "no_proxy"
+          value: "localhost,selenium,$no_proxy"
diff --git a/vars/npmExecuteTests.groovy b/vars/npmExecuteTests.groovy
new file mode 100644
index 000000000..bb34499c2
--- /dev/null
+++ b/vars/npmExecuteTests.groovy
@@ -0,0 +1,15 @@
+import groovy.transform.Field
+
+@Field String STEP_NAME = getClass().getName()
+@Field String METADATA_FILE = 'metadata/npmExecuteTests.yaml'
+
+@Field Set GENERAL_CONFIG_KEYS = []
+
+@Field Set STEP_CONFIG_KEYS = []
+
+@Field Set PARAMETER_KEYS = []
+
+void call(Map parameters = [:]) {
+    List credentials = []
+    piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials)
+}