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  }