github.com/saucelabs/saucectl@v0.175.1/internal/cmd/run/testcafe.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/region" 23 "github.com/saucelabs/saucectl/internal/report/captor" 24 "github.com/saucelabs/saucectl/internal/saucecloud" 25 "github.com/saucelabs/saucectl/internal/saucecloud/retry" 26 "github.com/saucelabs/saucectl/internal/segment" 27 "github.com/saucelabs/saucectl/internal/testcafe" 28 "github.com/saucelabs/saucectl/internal/usage" 29 "github.com/saucelabs/saucectl/internal/viper" 30 ) 31 32 type testcafeFlags struct { 33 QuarantineMode flags.QuarantineMode 34 Simulator flags.Simulator 35 } 36 37 // NewTestcafeCmd creates the 'run' command for TestCafe. 38 func NewTestcafeCmd() *cobra.Command { 39 sc := flags.SnakeCharmer{Fmap: map[string]*pflag.Flag{}} 40 lflags := testcafeFlags{} 41 42 cmd := &cobra.Command{ 43 Use: "testcafe", 44 Short: "Run testcafe tests", 45 SilenceUsage: true, 46 Hidden: true, // TODO reveal command once ready 47 TraverseChildren: true, 48 PreRunE: func(cmd *cobra.Command, args []string) error { 49 sc.BindAll() 50 return preRun() 51 }, 52 Run: func(cmd *cobra.Command, args []string) { 53 // Test patterns are passed in via positional args. 54 viper.Set("suite::src", args) 55 56 exitCode, err := runTestcafe(cmd, lflags, true) 57 if err != nil { 58 log.Err(err).Msg("failed to execute run command") 59 } 60 os.Exit(exitCode) 61 }, 62 } 63 64 f := cmd.Flags() 65 sc.Fset = cmd.Flags() 66 sc.String("name", "suite::name", "", "Set the name of the job as it will appear on Sauce Labs") 67 68 // TestCafe 69 sc.String("testcafe.version", "testcafe::version", "", "The TestCafe version to use") 70 sc.String("testcafe.configFile", "testcafe::configFile", "", "The path to TestCafe config file") 71 72 // Browser & Platform 73 sc.String("browser", "suite::browserName", "", "Run tests against this browser") 74 sc.String("browserVersion", "suite::browserVersion", "", "The browser version (default: latest)") 75 sc.StringSlice("browserArgs", "suite::browserArgs", []string{}, "Set browser args") 76 sc.String("platform", "suite::platformName", "", "Run tests against this platform") 77 sc.Bool("headless", "suite::headless", false, "Controls whether or not tests are run in headless mode (default: false)") 78 79 // Video & Screen(shots) 80 sc.Bool("disableScreenshots", "suite::disableScreenshots", false, "Prevent TestCafe from taking screenshots") 81 sc.String("screenResolution", "suite::screenResolution", "", "The screen resolution") 82 sc.Bool("screenshots.takeOnFails", "suite::screenshots::takeOnFails", false, "Take screenshot on test failure") 83 sc.Bool("screenshots.fullPage", "suite::screenshots::fullPage", false, "Take screenshots of the entire page") 84 85 // Error Handling 86 f.Var(&lflags.QuarantineMode, "quarantineMode", "Enable quarantine mode to eliminate false negatives and detect unstable tests") 87 sc.Bool("skipJsErrors", "suite::skipJsErrors", false, "Ignore JavaScript errors that occur on a tested web page") 88 sc.Bool("skipUncaughtErrors", "suite::skipUncaughtErrors", false, "Ignore uncaught errors or unhandled promise rejections on the server during test execution") 89 sc.Bool("stopOnFirstFail", "suite::stopOnFirstFail", false, "Stop an entire test run if any test fails") 90 91 // Timeouts 92 sc.Int("selectorTimeout", "suite::selectorTimeout", 10000, "Specify the time (in milliseconds) within which selectors attempt to return a node") 93 sc.Int("assertionTimeout", "suite::assertionTimeout", 3000, "Specify the time (in milliseconds) TestCafe attempts to successfully execute an assertion") 94 sc.Int("pageLoadTimeout", "suite::pageLoadTimeout", 3000, "Specify the time (in milliseconds) passed after the DOMContentLoaded event, within which TestCafe waits for the window.load event to fire") 95 sc.Int("ajaxRequestTimeout", "suite::ajaxRequestTimeout", 120000, "Specifies wait time (in milliseconds) for fetch/XHR requests") 96 sc.Int("pageRequestTimeout", "suite::pageRequestTimeout", 25000, "Specifies time (in milliseconds) to wait for HTML pages") 97 sc.Int("browserInitTimeout", "suite::browserInitTimeout", 120000, "Time (in milliseconds) for browsers to connect to TestCafe and report that they are ready to test") 98 sc.Int("testExecutionTimeout", "suite::testExecutionTimeout", 180000, "Maximum test execution time (in milliseconds)") 99 sc.Int("runExecutionTimeout", "suite::runExecutionTimeout", 1800000, "Maximum test run execution time (in milliseconds)") 100 101 // Filters 102 sc.String("filter.test", "suite::filter::test", "", "Runs a test with the specified name") 103 sc.String("filter.testGrep", "suite::filter::testGrep", "", "Runs tests whose names match the specified grep pattern") 104 sc.String("filter.fixture", "suite::filter::fixture", "", "Runs a test with the specified fixture name") 105 sc.String("filter.fixtureGrep", "suite::filter::fixtureGrep", "", "Runs tests whose fixture names match the specified grep pattern") 106 sc.StringToString("filter.testMeta", "suite::filter::testMeta", map[string]string{}, "Runs tests whose metadata matches the specified key-value pair") 107 sc.StringToString("filter.fixtureMeta", "suite::filter::fixtureMeta", map[string]string{}, "Runs tests whose fixture’s metadata matches the specified key-value pair") 108 109 // Misc 110 sc.String("rootDir", "rootDir", ".", "Control what files are available in the context of a test run, unless explicitly excluded by .sauceignore") 111 sc.StringSlice("clientScripts", "suite::clientScripts", []string{}, "Inject scripts from the specified files into each page visited during the tests") 112 sc.Float64("speed", "suite::speed", 1, "Specify the test execution speed") 113 sc.Bool("disablePageCaching", "suite::disablePageCaching", false, "Prevent the browser from caching page content") 114 sc.StringSlice("excludedTestFiles", "suite::excludedTestFiles", []string{}, "Exclude test files to skip the tests, using glob pattern") 115 sc.String("timeZone", "suite::timeZone", "", "Specifies timeZone for this test") 116 sc.Int("passThreshold", "suite::passThreshold", 1, "The minimum number of successful attempts for a suite to be considered as 'passed'.") 117 118 // NPM 119 sc.String("npm.registry", "npm::registry", "", "Specify the npm registry URL") 120 sc.StringToString("npm.packages", "npm::packages", map[string]string{}, "Specify npm packages that are required to run tests") 121 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.") 122 sc.Bool("npm.strictSSL", "npm::strictSSL", true, "Whether or not to do SSL key validation when making requests to the registry via https") 123 124 // Simulators 125 f.Var(&lflags.Simulator, "simulator", "Specifies the simulator to use for testing") 126 127 return cmd 128 } 129 130 func runTestcafe(cmd *cobra.Command, tcFlags testcafeFlags, isCLIDriven bool) (int, error) { 131 if !isCLIDriven { 132 config.ValidateSchema(gFlags.cfgFilePath) 133 } 134 135 p, err := testcafe.FromFile(gFlags.cfgFilePath) 136 if err != nil { 137 return 1, err 138 } 139 140 p.CLIFlags = flags.CaptureCommandLineFlags(cmd.Flags()) 141 142 if err := applyTestcafeFlags(&p, tcFlags); err != nil { 143 return 1, err 144 } 145 testcafe.SetDefaults(&p) 146 147 if err := testcafe.Validate(&p); err != nil { 148 return 1, err 149 } 150 151 regio := region.FromString(p.Sauce.Region) 152 if regio == region.USEast4 { 153 return 1, errors.New(msg.NoFrameworkSupport) 154 } 155 156 if !gFlags.noAutoTagging { 157 p.Sauce.Metadata.Tags = append(p.Sauce.Metadata.Tags, ci.GetTags()...) 158 } 159 160 tracker := segment.DefaultTracker 161 if regio == region.Staging { 162 tracker.Enabled = false 163 } 164 165 go func() { 166 props := usage.Properties{} 167 props.SetFramework("testcafe").SetFVersion(p.Testcafe.Version).SetFlags(cmd.Flags()).SetSauceConfig(p.Sauce). 168 SetArtifacts(p.Artifacts).SetNPM(p.Npm).SetNumSuites(len(p.Suites)).SetJobs(captor.Default.TestResults). 169 SetSlack(p.Notifications.Slack).SetSharding(testcafe.IsSharded(p.Suites)).SetLaunchOrder(p.Sauce.LaunchOrder). 170 SetSmartRetry(p.IsSmartRetried()).SetReporters(p.Reporters) 171 tracker.Collect(cases.Title(language.English).String(cmds.FullName(cmd)), props) 172 _ = tracker.Close() 173 }() 174 175 cleanupArtifacts(p.Artifacts) 176 177 creds := regio.Credentials() 178 179 restoClient := http.NewResto(regio.APIBaseURL(), creds.Username, creds.AccessKey, 0) 180 restoClient.ArtifactConfig = p.Artifacts.Download 181 testcompClient := http.NewTestComposer(regio.APIBaseURL(), creds, testComposerTimeout) 182 webdriverClient := http.NewWebdriver(regio.WebDriverBaseURL(), creds, webdriverTimeout) 183 appsClient := *http.NewAppStore(regio.APIBaseURL(), creds.Username, creds.AccessKey, gFlags.appStoreTimeout) 184 rdcClient := http.NewRDCService(regio.APIBaseURL(), creds.Username, creds.AccessKey, rdcTimeout, config.ArtifactDownload{}) 185 insightsClient := http.NewInsightsService(regio.APIBaseURL(), creds, insightsTimeout) 186 iamClient := http.NewUserService(regio.APIBaseURL(), creds, iamTimeout) 187 188 log.Info().Msg("Running Testcafe in Sauce Labs") 189 r := saucecloud.TestcafeRunner{ 190 Project: p, 191 CloudRunner: saucecloud.CloudRunner{ 192 ProjectUploader: &appsClient, 193 JobService: saucecloud.JobService{ 194 VDCStarter: &webdriverClient, 195 RDCStarter: &rdcClient, 196 VDCReader: &restoClient, 197 RDCReader: &rdcClient, 198 VDCWriter: &testcompClient, 199 VDCStopper: &restoClient, 200 RDCStopper: &rdcClient, 201 VDCDownloader: &restoClient, 202 }, 203 TunnelService: &restoClient, 204 MetadataService: &testcompClient, 205 InsightsService: &insightsClient, 206 UserService: &iamClient, 207 BuildService: &restoClient, 208 Region: regio, 209 ShowConsoleLog: p.ShowConsoleLog, 210 Reporters: createReporters(p.Reporters, p.Notifications, p.Sauce.Metadata, &testcompClient, &restoClient, 211 "testcafe", "sauce", gFlags.async), 212 Async: gFlags.async, 213 FailFast: gFlags.failFast, 214 MetadataSearchStrategy: framework.NewSearchStrategy(p.Testcafe.Version, p.RootDir), 215 NPMDependencies: p.Npm.Dependencies, 216 Retrier: &retry.SauceReportRetrier{ 217 VDCReader: &restoClient, 218 ProjectUploader: &appsClient, 219 Project: &p, 220 }, 221 }, 222 } 223 224 cleanTestCafePackages(&p) 225 return r.RunProject() 226 } 227 228 func applyTestcafeFlags(p *testcafe.Project, flags testcafeFlags) error { 229 if gFlags.selectedSuite != "" { 230 if err := testcafe.FilterSuites(p, gFlags.selectedSuite); err != nil { 231 return err 232 } 233 } 234 235 if p.Suite.Name == "" { 236 return nil 237 } 238 239 if flags.QuarantineMode.Changed { 240 p.Suite.QuarantineMode = flags.QuarantineMode.Values 241 } 242 243 if flags.Simulator.Changed { 244 p.Suite.Simulators = []config.Simulator{flags.Simulator.Simulator} 245 } 246 247 p.Suites = []testcafe.Suite{p.Suite} 248 249 return nil 250 } 251 252 func cleanTestCafePackages(p *testcafe.Project) { 253 version, hasFramework := p.Npm.Packages["testcafe"] 254 if hasFramework { 255 log.Warn().Msg(msg.IgnoredNpmPackagesMsg("testcafe", p.Testcafe.Version, []string{fmt.Sprintf("testcafe@%s", version)})) 256 p.Npm.Packages = config.CleanNpmPackages(p.Npm.Packages, []string{"testcafe"}) 257 } 258 }