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 }