github.com/saucelabs/saucectl@v0.175.1/internal/cmd/run/playwright.go (about)

     1  package run
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  
     8  	cmds "github.com/saucelabs/saucectl/internal/cmd"
     9  	"github.com/saucelabs/saucectl/internal/http"
    10  
    11  	"github.com/rs/zerolog/log"
    12  	"github.com/spf13/cobra"
    13  	"github.com/spf13/pflag"
    14  	"golang.org/x/text/cases"
    15  	"golang.org/x/text/language"
    16  
    17  	"github.com/saucelabs/saucectl/internal/ci"
    18  	"github.com/saucelabs/saucectl/internal/config"
    19  	"github.com/saucelabs/saucectl/internal/flags"
    20  	"github.com/saucelabs/saucectl/internal/framework"
    21  	"github.com/saucelabs/saucectl/internal/msg"
    22  	"github.com/saucelabs/saucectl/internal/playwright"
    23  	"github.com/saucelabs/saucectl/internal/region"
    24  	"github.com/saucelabs/saucectl/internal/report/captor"
    25  	"github.com/saucelabs/saucectl/internal/saucecloud"
    26  	"github.com/saucelabs/saucectl/internal/saucecloud/retry"
    27  	"github.com/saucelabs/saucectl/internal/segment"
    28  	"github.com/saucelabs/saucectl/internal/usage"
    29  	"github.com/saucelabs/saucectl/internal/viper"
    30  )
    31  
    32  // NewPlaywrightCmd creates the 'run' command for Playwright.
    33  func NewPlaywrightCmd() *cobra.Command {
    34  	sc := flags.SnakeCharmer{Fmap: map[string]*pflag.Flag{}}
    35  
    36  	cmd := &cobra.Command{
    37  		Use:              "playwright",
    38  		Short:            "Run playwright tests",
    39  		SilenceUsage:     true,
    40  		Hidden:           true, // TODO reveal command once ready
    41  		TraverseChildren: true,
    42  		PreRunE: func(cmd *cobra.Command, args []string) error {
    43  			sc.BindAll()
    44  			return preRun()
    45  		},
    46  		Run: func(cmd *cobra.Command, args []string) {
    47  			// Test patterns are passed in via positional args.
    48  			viper.Set("suite::testMatch", args)
    49  
    50  			exitCode, err := runPlaywright(cmd, true)
    51  			if err != nil {
    52  				log.Err(err).Msg("failed to execute run command")
    53  			}
    54  			os.Exit(exitCode)
    55  		},
    56  	}
    57  
    58  	sc.Fset = cmd.Flags()
    59  
    60  	sc.String("name", "suite::name", "", "Set the name of the job as it will appear on Sauce Labs")
    61  
    62  	// Browser & Platform
    63  	sc.String("browser", "suite::params::browserName", "", "Run tests against this browser")
    64  	sc.String("platform", "suite::platformName", "", "Run tests against this platform")
    65  
    66  	// Playwright
    67  	sc.String("playwright.version", "playwright::version", "", "The Playwright version to use")
    68  	sc.String("playwright.configFile", "playwright::configFile", "", "The path to playwright config file")
    69  
    70  	// Playwright Test Options
    71  	sc.Bool("headless", "suite::params::headless", false, "Run tests in headless mode")
    72  	sc.Int("globalTimeout", "suite::params::globalTimeout", 0, "Total timeout for the whole test run in milliseconds")
    73  	sc.Int("testTimeout", "suite::params::timeout", 0, "Maximum timeout in milliseconds for each test")
    74  	sc.String("grep", "suite::params::grep", "", "Only run tests matching this regular expression")
    75  	sc.String("grep-invert", "suite::params::grepInvert", "", "Only run tests not matching this regular expression. ")
    76  	sc.Int("repeatEach", "suite::params::repeatEach", 0, "Run each test N times")
    77  	sc.Int("retries", "suite::params::retries", 0, "The maximum number of retries for flaky tests")
    78  	sc.Int("maxFailures", "suite::params::maxFailures", 0, "Stop after the first N test failures")
    79  	sc.Int("numShards", "suite::numShards", 0, "Split tests across N number of shards")
    80  	sc.String("project", "suite::params::project", "", "Specify playwright project")
    81  	sc.StringSlice("excludedTestFiles", "suite::excludedTestFiles", []string{}, "Exclude test files to skip the tests, using regex")
    82  	sc.Bool("updateSnapshots", "suite::params::updateSnapshots", false, "Whether to update expected snapshots with the actual results produced by the test run.")
    83  	sc.Int("workers", "suite::params::workers", 1, "Set the maximum number of parallel worker processes (Default: 1).")
    84  
    85  	// Misc
    86  	sc.String("rootDir", "rootDir", ".", "Control what files are available in the context of a test run, unless explicitly excluded by .sauceignore")
    87  	sc.String("shard", "suite.shard", "", "Controls whether or not (and how) tests are sharded across multiple machines, supported value: spec|concurrency")
    88  	sc.String("timeZone", "suite::timeZone", "", "Specifies timeZone for this test")
    89  	sc.Int("passThreshold", "suite::passThreshold", 1, "The minimum number of successful attempts for a suite to be considered as 'passed'.")
    90  	sc.Bool("shardGrepEnabled", "suite::shardGrepEnabled", false, "When sharding is configured and the suite is configured to filter using a pattern, let saucectl filter tests before executing")
    91  
    92  	// NPM
    93  	sc.String("npm.registry", "npm::registry", "", "Specify the npm registry URL")
    94  	sc.StringToString("npm.packages", "npm::packages", map[string]string{}, "Specify npm packages that are required to run tests")
    95  	sc.StringSlice("npm.dependencies", "npm::dependencies", []string{}, "Specify local npm dependencies for saucectl to upload. These dependencies must already be installed in the local node_modules directory.")
    96  	sc.Bool("npm.strictSSL", "npm::strictSSL", true, "Whether or not to do SSL key validation when making requests to the registry via https")
    97  
    98  	return cmd
    99  }
   100  
   101  func runPlaywright(cmd *cobra.Command, isCLIDriven bool) (int, error) {
   102  	if !isCLIDriven {
   103  		config.ValidateSchema(gFlags.cfgFilePath)
   104  	}
   105  
   106  	p, err := playwright.FromFile(gFlags.cfgFilePath)
   107  	if err != nil {
   108  		return 1, err
   109  	}
   110  
   111  	p.CLIFlags = flags.CaptureCommandLineFlags(cmd.Flags())
   112  
   113  	if err := applyPlaywrightFlags(&p); err != nil {
   114  		return 1, err
   115  	}
   116  	playwright.SetDefaults(&p)
   117  
   118  	if err := playwright.Validate(&p); err != nil {
   119  		return 1, err
   120  	}
   121  
   122  	if err := playwright.ShardSuites(&p); err != nil {
   123  		return 1, err
   124  	}
   125  
   126  	regio := region.FromString(p.Sauce.Region)
   127  	if regio == region.USEast4 {
   128  		return 1, errors.New(msg.NoFrameworkSupport)
   129  	}
   130  
   131  	if !gFlags.noAutoTagging {
   132  		p.Sauce.Metadata.Tags = append(p.Sauce.Metadata.Tags, ci.GetTags()...)
   133  	}
   134  
   135  	tracker := segment.DefaultTracker
   136  	if regio == region.Staging {
   137  		tracker.Enabled = false
   138  	}
   139  
   140  	go func() {
   141  		props := usage.Properties{}
   142  		props.SetFramework("playwright").SetFVersion(p.Playwright.Version).SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce).
   143  			SetArtifacts(p.Artifacts).SetNPM(p.Npm).SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults).
   144  			SetSlack(p.Notifications.Slack).SetSharding(playwright.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder).
   145  			SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters)
   146  		tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props)
   147  		_ = tracker.Close()
   148  	}()
   149  
   150  	cleanupArtifacts(p.Artifacts)
   151  
   152  	creds := regio.Credentials()
   153  
   154  	restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0)
   155  	restoClient.ArtifactConfig = p.Artifacts.Download
   156  	testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout)
   157  	webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout)
   158  	appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout)
   159  	rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{})
   160  	insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout)
   161  	iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout)
   162  
   163  	log.Info().Msg("Running Playwright in Sauce Labs")
   164  	r := saucecloud.PlaywrightRunner{
   165  		Project: p,
   166  		CloudRunner: saucecloud.CloudRunner{
   167  			ProjectUploader: &appsClient,
   168  			JobService: saucecloud.JobService{
   169  				VDCStarter:    &webdriverClient,
   170  				RDCStarter:    &rdcClient,
   171  				VDCReader:     &restoClient,
   172  				RDCReader:     &rdcClient,
   173  				VDCWriter:     &testcompClient,
   174  				VDCStopper:    &restoClient,
   175  				RDCStopper:    &rdcClient,
   176  				VDCDownloader: &restoClient,
   177  			},
   178  			TunnelService:   &restoClient,
   179  			MetadataService: &testcompClient,
   180  			InsightsService: &insightsClient,
   181  			UserService:     &iamClient,
   182  			BuildService:    &restoClient,
   183  			Region:          regio,
   184  			ShowConsoleLog:  p.ShowConsoleLog,
   185  			Reporters: createReporters(p.Reporters, p.Notifications, p.Sauce.Metadata, &testcompClient, &restoClient,
   186  				"playwright", "sauce", gFlags.async),
   187  			Async:                  gFlags.async,
   188  			FailFast:               gFlags.failFast,
   189  			MetadataSearchStrategy: framework.NewSearchStrategy(p.Playwright.Version, p.RootDir),
   190  			NPMDependencies:        p.Npm.Dependencies,
   191  			Retrier: &retry.SauceReportRetrier{
   192  				VDCReader:       &restoClient,
   193  				ProjectUploader: &appsClient,
   194  				Project:         &p,
   195  			},
   196  		},
   197  	}
   198  
   199  	p.Npm.Packages = cleanPlaywrightPackages(p.Npm, p.Playwright.Version)
   200  	return r.RunProject()
   201  }
   202  
   203  func applyPlaywrightFlags(p *playwright.Project) error {
   204  	if gFlags.selectedSuite != "" {
   205  		if err := playwright.FilterSuites(p, gFlags.selectedSuite); err != nil {
   206  			return err
   207  		}
   208  	}
   209  
   210  	// Use the adhoc suite instead, if one is provided
   211  	if p.Suite.Name != "" {
   212  		p.Suites = []playwright.Suite{p.Suite}
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  func cleanPlaywrightPackages(n config.Npm, version string) map[string]string {
   219  	// Don't allow framework installation, it is provided by the runner
   220  	ignoredPackages := []string{}
   221  	playwrightVersion, hasPlaywright := n.Packages["playwright"]
   222  	playwrightTestVersion, hasPlaywrightTest := n.Packages["@playwright/test"]
   223  	if hasPlaywright {
   224  		ignoredPackages = append(ignoredPackages, fmt.Sprintf("playwright@%s", playwrightVersion))
   225  	}
   226  	if hasPlaywrightTest {
   227  		ignoredPackages = append(ignoredPackages, fmt.Sprintf("@playwright/test@%s", playwrightTestVersion))
   228  	}
   229  	if hasPlaywright || hasPlaywrightTest {
   230  		log.Warn().Msg(msg.IgnoredNpmPackagesMsg("playwright", version, ignoredPackages))
   231  		return config.CleanNpmPackages(n.Packages, []string{"playwright", "@playwright/test"})
   232  	}
   233  	return n.Packages
   234  }