How to use startCtDevServer method in Cypress

Best JavaScript code snippet using cypress

project_spec.js

Source:project_spec.js Github

copy

Full Screen

1require('../spec_helper')2const mockedEnv = require('mocked-env')3const path = require('path')4const commitInfo = require('@cypress/commit-info')5const chokidar = require('chokidar')6const pkg = require('@packages/root')7const Fixtures = require('@tooling/system-tests/lib/fixtures')8const { sinon } = require('../spec_helper')9const api = require(`${root}lib/api`)10const user = require(`${root}lib/user`)11const cache = require(`${root}lib/cache`)12const config = require(`${root}lib/config`)13const scaffold = require(`${root}lib/scaffold`)14const { ServerE2E } = require(`${root}lib/server-e2e`)15const { ProjectBase } = require(`${root}lib/project-base`)16const {17  getOrgs,18  paths,19  remove,20  add,21  getId,22  getPathsAndIds,23  getProjectStatus,24  getProjectStatuses,25  createCiProject,26  writeProjectId,27} = require(`${root}lib/project_static`)28const ProjectUtils = require(`${root}lib/project_utils`)29const { Automation } = require(`${root}lib/automation`)30const savedState = require(`${root}lib/saved_state`)31const plugins = require(`${root}lib/plugins`)32const runEvents = require(`${root}lib/plugins/run_events`)33const system = require(`${root}lib/util/system`)34const { fs } = require(`${root}lib/util/fs`)35const settings = require(`${root}lib/util/settings`)36const Watchers = require(`${root}lib/watchers`)37const { SocketE2E } = require(`${root}lib/socket-e2e`)38describe('lib/project-base', () => {39  beforeEach(function () {40    Fixtures.scaffold()41    this.todosPath = Fixtures.projectPath('todos')42    this.idsPath = Fixtures.projectPath('ids')43    this.pristinePath = Fixtures.projectPath('pristine')44    sinon.stub(scaffold, 'isNewProject').resolves(false)45    sinon.stub(chokidar, 'watch').returns({46      on: () => {},47      close: () => {},48    })49    sinon.stub(runEvents, 'execute').resolves()50    return settings.read(this.todosPath).then((obj = {}) => {51      ({ projectId: this.projectId } = obj)52      return config.set({ projectName: 'project', projectRoot: '/foo/bar' })53      .then((config1) => {54        this.config = config155        this.project = new ProjectBase({ projectRoot: this.todosPath, testingType: 'e2e' })56        this.project._server = { close () {} }57        this.project._cfg = config158      })59    })60  })61  afterEach(function () {62    Fixtures.remove()63    if (this.project) {64      this.project.close()65    }66  })67  it('requires a projectRoot', function () {68    const fn = () => new ProjectBase({})69    expect(fn).to.throw('Instantiating lib/project requires a projectRoot!')70  })71  it('always resolves the projectRoot to be absolute', function () {72    const p = new ProjectBase({ projectRoot: '../foo/bar', testingType: 'e2e' })73    expect(p.projectRoot).not.to.eq('../foo/bar')74    expect(p.projectRoot).to.eq(path.resolve('../foo/bar'))75  })76  it('sets CT specific defaults and calls CT function', async function () {77    sinon.stub(ServerE2E.prototype, 'open').resolves([])78    sinon.stub(ProjectBase.prototype, 'startCtDevServer').resolves({ port: 9999 })79    const projectCt = new ProjectBase({ projectRoot: this.pristinePath, testingType: 'component' })80    await projectCt.initializeConfig()81    return projectCt.open({}).then(() => {82      expect(projectCt._cfg.viewportHeight).to.eq(500)83      expect(projectCt._cfg.viewportWidth).to.eq(500)84      expect(projectCt._cfg.baseUrl).to.eq('http://localhost:9999')85      expect(projectCt.startCtDevServer).to.have.beenCalled86    })87  })88  context('#saveState', function () {89    beforeEach(function () {90      const integrationFolder = 'the/save/state/test'91      sinon.stub(config, 'get').withArgs(this.todosPath).resolves({ integrationFolder })92      this.project.cfg = { integrationFolder }93      return savedState.create(this.project.projectRoot)94      .then((state) => state.remove())95    })96    afterEach(function () {97      return savedState.create(this.project.projectRoot)98      .then((state) => state.remove())99    })100    it('saves state without modification', function () {101      return this.project.saveState()102      .then((state) => expect(state).to.deep.eq({}))103    })104    it('adds property', function () {105      return this.project.saveState()106      .then(() => this.project.saveState({ appWidth: 42 }))107      .then((state) => expect(state).to.deep.eq({ appWidth: 42 }))108    })109    it('adds second property', function () {110      return this.project.saveState()111      .then(() => this.project.saveState({ appWidth: 42 }))112      .then(() => this.project.saveState({ appHeight: true }))113      .then((state) => expect(state).to.deep.eq({ appWidth: 42, appHeight: true }))114    })115    it('modifes property', function () {116      return this.project.saveState()117      .then(() => this.project.saveState({ appWidth: 42 }))118      .then(() => this.project.saveState({ appWidth: 'modified' }))119      .then((state) => expect(state).to.deep.eq({ appWidth: 'modified' }))120    })121  })122  context('#initializeConfig', () => {123    const integrationFolder = 'foo/bar/baz'124    beforeEach(function () {125      sinon.stub(config, 'get').withArgs(this.todosPath, { foo: 'bar', configFile: 'cypress.json' })126      .resolves({ baz: 'quux', integrationFolder, browsers: [] })127    })128    it('calls config.get with projectRoot + options + saved state', function () {129      this.project.__setOptions({ foo: 'bar' })130      return savedState.create(this.todosPath)131      .then(async (state) => {132        sinon.stub(state, 'get').resolves({ reporterWidth: 225 })133        await this.project.initializeConfig()134        expect(this.project.getConfig()).to.deep.eq({135          integrationFolder,136          browsers: [],137          isNewProject: false,138          baz: 'quux',139          state: {140            reporterWidth: 225,141          },142        })143      })144    })145    it('resolves if cfg is already set', async function () {146      this.project._cfg = {147        integrationFolder,148        foo: 'bar',149      }150      expect(this.project.getConfig()).to.deep.eq({151        integrationFolder,152        foo: 'bar',153      })154    })155    it('sets cfg.isNewProject to false when state.showedNewProjectBanner is true', function () {156      this.project.__setOptions({ foo: 'bar' })157      return savedState.create(this.todosPath)158      .then((state) => {159        sinon.stub(state, 'get').resolves({ showedNewProjectBanner: true })160        return this.project.initializeConfig()161        .then((cfg) => {162          expect(cfg).to.deep.eq({163            integrationFolder,164            browsers: [],165            isNewProject: false,166            baz: 'quux',167            state: {168              showedNewProjectBanner: true,169            },170          })171          this.project._cfg = cfg172        })173      })174    })175    it('does not set cfg.isNewProject when cfg.isTextTerminal', function () {176      const cfg = { isTextTerminal: true, browsers: [] }177      config.get.resolves(cfg)178      sinon.stub(this.project, '_setSavedState').resolves(cfg)179      return this.project.initializeConfig()180      .then((cfg) => {181        expect(cfg).not.to.have.property('isNewProject')182      })183    })184    it('attaches warning to non-chrome browsers when chromeWebSecurity:false', async function () {185      const cfg = Object.assign({}, {186        integrationFolder,187        browsers: [{ family: 'chromium', name: 'Canary' }, { family: 'some-other-family', name: 'some-other-name' }],188        chromeWebSecurity: false,189      })190      config.get.restore()191      sinon.stub(config, 'get').returns(cfg)192      await this.project.initializeConfig()193      .then(() => {194        const cfg = this.project.getConfig()195        expect(cfg.chromeWebSecurity).eq(false)196        expect(cfg.browsers).deep.eq([197          {198            family: 'chromium',199            name: 'Canary',200          },201          {202            family: 'some-other-family',203            name: 'some-other-name',204            warning: `\205Your project has set the configuration option: \`chromeWebSecurity: false\`206This option will not have an effect in Some-other-name. Tests that rely on web security being disabled will not run as expected.\207`,208          },209        ])210        expect(cfg).ok211      })212    })213    // https://github.com/cypress-io/cypress/issues/17614214    it('only attaches warning to non-chrome browsers when chromeWebSecurity:true', async function () {215      config.get.restore()216      sinon.stub(config, 'get').returns({217        integrationFolder,218        browsers: [{ family: 'chromium', name: 'Canary' }, { family: 'some-other-family', name: 'some-other-name' }],219        chromeWebSecurity: true,220      })221      await this.project.initializeConfig()222      .then(() => {223        const cfg = this.project.getConfig()224        expect(cfg.chromeWebSecurity).eq(true)225        expect(cfg.browsers).deep.eq([226          {227            family: 'chromium',228            name: 'Canary',229          },230          {231            family: 'some-other-family',232            name: 'some-other-name',233          },234        ])235      })236    })237  })238  context('#initializeConfig', function () {239  })240  context('#open', () => {241    beforeEach(function () {242      sinon.stub(this.project, 'watchSettings')243      sinon.stub(this.project, 'startWebsockets')244      this.checkSupportFileStub = sinon.stub(ProjectUtils, 'checkSupportFile').resolves()245      sinon.stub(this.project, 'scaffold').resolves()246      sinon.stub(this.project, 'getConfig').returns(this.config)247      sinon.stub(ServerE2E.prototype, 'open').resolves([])248      sinon.stub(ServerE2E.prototype, 'reset')249      sinon.stub(config, 'updateWithPluginValues').returns(this.config)250      sinon.stub(scaffold, 'plugins').resolves()251      sinon.stub(plugins, 'init').resolves()252    })253    it('calls #watchSettings with options + config', function () {254      return this.project.open().then(() => {255        expect(this.project.watchSettings).to.be.calledWith({256          configFile: undefined,257          onSettingsChanged: false,258          projectRoot: this.todosPath,259        })260      })261    })262    it('calls #startWebsockets with options + config', function () {263      const onFocusTests = sinon.stub()264      this.project.__setOptions({265        onFocusTests,266      })267      return this.project.open().then(() => {268        expect(this.project.startWebsockets).to.be.calledWith({269          onReloadBrowser: undefined,270          onFocusTests,271          onSpecChanged: undefined,272        }, {273          socketIoCookie: '__socket.io',274          namespace: '__cypress',275          screenshotsFolder: '/foo/bar/cypress/screenshots',276          report: undefined,277          reporter: 'spec',278          reporterOptions: null,279          projectRoot: this.todosPath,280        })281      })282    })283    it('calls #scaffold with server config promise', function () {284      return this.project.open().then(() => {285        expect(this.project.scaffold).to.be.calledWith(this.config)286      })287    })288    it('calls checkSupportFile with server config when scaffolding is finished', function () {289      return this.project.open().then(() => {290        expect(this.checkSupportFileStub).to.be.calledWith({291          configFile: 'cypress.json',292          supportFile: '/foo/bar/cypress/support/index.js',293        })294      })295    })296    it('initializes the plugins', function () {297      return this.project.open().then(() => {298        expect(plugins.init).to.be.called299      })300    })301    it('calls support.plugins with pluginsFile directory', function () {302      return this.project.open().then(() => {303        expect(scaffold.plugins).to.be.calledWith(path.dirname(this.config.pluginsFile))304      })305    })306    it('calls options.onError with plugins error when there is a plugins error', function () {307      const onError = sinon.spy()308      const err = {309        name: 'plugin error name',310        message: 'plugin error message',311      }312      this.project.__setOptions({ onError })313      return this.project.open().then(() => {314        const pluginsOnError = plugins.init.lastCall.args[1].onError315        expect(pluginsOnError).to.be.a('function')316        pluginsOnError(err)317        expect(onError).to.be.calledWith(err)318      })319    })320    // TODO: skip this for now321    it.skip('watches cypress.json', function () {322      return this.server.open().bind(this).then(() => {323        expect(Watchers.prototype.watch).to.be.calledWith('/Users/brian/app/cypress.json')324      })325    })326    // TODO: skip this for now327    it.skip('passes watchers to Socket.startListening', function () {328      const options = {}329      return this.server.open(options).then(() => {330        const { startListening } = SocketE2E.prototype331        expect(startListening.getCall(0).args[0]).to.be.instanceof(Watchers)332        expect(startListening.getCall(0).args[1]).to.eq(options)333      })334    })335    it('executes before:run if in interactive mode', function () {336      const sysInfo = {337        osName: 'darwin',338        osVersion: '1.2.3',339      }340      sinon.stub(system, 'info').resolves(sysInfo)341      this.config.experimentalInteractiveRunEvents = true342      this.config.isTextTerminal = false343      return this.project.open()344      .then(() => {345        expect(runEvents.execute).to.be.calledWith('before:run', this.config, {346          config: this.config,347          cypressVersion: pkg.version,348          system: sysInfo,349        })350      })351    })352    it('does not get system info or execute before:run if not in interactive mode', function () {353      sinon.stub(system, 'info')354      this.config.experimentalInteractiveRunEvents = true355      this.config.isTextTerminal = true356      return this.project.open()357      .then(() => {358        expect(system.info).not.to.be.called359        expect(runEvents.execute).not.to.be.calledWith('before:run')360      })361    })362    it('does not call startSpecWatcher if not in interactive mode', function () {363      const startSpecWatcherStub = sinon.stub()364      sinon.stub(ProjectBase.prototype, 'initializeSpecStore').resolves({365        startSpecWatcher: startSpecWatcherStub,366      })367      this.config.isTextTerminal = true368      return this.project.open()369      .then(() => {370        expect(startSpecWatcherStub).not.to.be.called371      })372    })373    it('calls startSpecWatcher if in interactive mode', function () {374      const startSpecWatcherStub = sinon.stub()375      sinon.stub(ProjectBase.prototype, 'initializeSpecStore').resolves({376        startSpecWatcher: startSpecWatcherStub,377      })378      this.config.isTextTerminal = false379      return this.project.open()380      .then(() => {381        expect(startSpecWatcherStub).to.be.called382      })383    })384    it('does not get system info or execute before:run if experimental flag is not enabled', function () {385      sinon.stub(system, 'info')386      this.config.experimentalInteractiveRunEvents = false387      this.config.isTextTerminal = false388      return this.project.open()389      .then(() => {390        expect(system.info).not.to.be.called391        expect(runEvents.execute).not.to.be.calledWith('before:run')392      })393    })394    describe('saved state', function () {395      beforeEach(function () {396        this._time = 1609459200000397        this._dateStub = sinon.stub(Date, 'now').returns(this._time)398      })399      it('sets firstOpened and lastOpened on first open', function () {400        return this.project.open()401        .then(() => {402          const cfg = this.project.getConfig()403          expect(cfg.state).to.eql({ firstOpened: this._time, lastOpened: this._time })404        })405      })406      it('only sets lastOpened on subsequent opens', function () {407        return this.project.open()408        .then(() => {409          this._dateStub.returns(this._time + 100000)410        })411        .then(() => this.project.open())412        .then(() => {413          const cfg = this.project.getConfig()414          expect(cfg.state).to.eql({ firstOpened: this._time, lastOpened: this._time + 100000 })415        })416      })417      it('updates config.state when saved state changes', function () {418        sinon.spy(this.project, 'saveState')419        const options = { onSavedStateChanged: (...args) => this.project.saveState(...args) }420        this.project.__setOptions(options)421        return this.project.open()422        .then(() => options.onSavedStateChanged({ autoScrollingEnabled: false }))423        .then(() => {424          const cfg = this.project.getConfig()425          expect(this.project.saveState).to.be.calledWith({ autoScrollingEnabled: false })426          expect(cfg.state).to.eql({427            autoScrollingEnabled: false,428            firstOpened: this._time,429            lastOpened: this._time,430          })431        })432      })433    })434  })435  context('#close', () => {436    beforeEach(function () {437      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })438      this.project._server = { close () {} }439      this.project._isServerOpen = true440      sinon.stub(this.project, 'getConfig').returns(this.config)441      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')442    })443    it('closes server', function () {444      this.project._server = sinon.stub({ close () {} })445      return this.project.close().then(() => {446        expect(this.project._server.close).to.be.calledOnce447      })448    })449    it('closes watchers', function () {450      this.project.watchers = sinon.stub({ close () {} })451      return this.project.close().then(() => {452        expect(this.project.watchers.close).to.be.calledOnce453      })454    })455    it('can close when server + watchers arent open', function () {456      return this.project.close()457    })458    it('executes after:run if in interactive mode', function () {459      this.config.experimentalInteractiveRunEvents = true460      this.config.isTextTerminal = false461      return this.project.close()462      .then(() => {463        expect(runEvents.execute).to.be.calledWith('after:run', this.config)464      })465    })466    it('does not execute after:run if not in interactive mode', function () {467      this.config.experimentalInteractiveRunEvents = true468      this.config.isTextTerminal = true469      return this.project.close()470      .then(() => {471        expect(runEvents.execute).not.to.be.calledWith('after:run')472      })473    })474    it('does not execute after:run if experimental flag is not enabled', function () {475      this.config.experimentalInteractiveRunEvents = false476      this.config.isTextTerminal = false477      return this.project.close()478      .then(() => {479        expect(runEvents.execute).not.to.be.calledWith('after:run')480      })481    })482  })483  context('#reset', () => {484    beforeEach(function () {485      this.project = new ProjectBase({ projectRoot: this.pristinePath, testingType: 'e2e' })486      this.project._automation = { reset: sinon.stub() }487      this.project._server = { close () {}, reset: sinon.stub() }488    })489    it('resets server + automation', function () {490      this.project.reset()491      expect(this.project._automation.reset).to.be.calledOnce492      expect(this.project.server.reset).to.be.calledOnce493    })494  })495  context('#getRuns', () => {496    beforeEach(function () {497      this.project = new ProjectBase({ projectRoot: this.pristinePath, testingType: 'e2e' })498      sinon.stub(settings, 'read').resolves({ projectId: 'id-123' })499      sinon.stub(api, 'getProjectRuns').resolves('runs')500      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')501    })502    it('calls api.getProjectRuns with project id + session', function () {503      return this.project.getRuns().then((runs) => {504        expect(api.getProjectRuns).to.be.calledWith('id-123', 'auth-token-123')505        expect(runs).to.equal('runs')506      })507    })508  })509  context('#scaffold', () => {510    beforeEach(function () {511      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })512      sinon.stub(scaffold, 'integration').resolves()513      sinon.stub(scaffold, 'fixture').resolves()514      sinon.stub(scaffold, 'support').resolves()515      sinon.stub(scaffold, 'plugins').resolves()516      this.obj = { projectRoot: 'pr', fixturesFolder: 'ff', integrationFolder: 'if', supportFolder: 'sf', pluginsFile: 'pf/index.js' }517    })518    it('calls scaffold.integration with integrationFolder', function () {519      return this.project.scaffold(this.obj).then(() => {520        expect(scaffold.integration).to.be.calledWith(this.obj.integrationFolder)521      })522    })523    it('calls fixture.scaffold with fixturesFolder', function () {524      return this.project.scaffold(this.obj).then(() => {525        expect(scaffold.fixture).to.be.calledWith(this.obj.fixturesFolder)526      })527    })528    it('calls support.scaffold with supportFolder', function () {529      return this.project.scaffold(this.obj).then(() => {530        expect(scaffold.support).to.be.calledWith(this.obj.supportFolder)531      })532    })533    it('does not call support.plugins if config.pluginsFile is falsey', function () {534      this.obj.pluginsFile = false535      return this.project.scaffold(this.obj).then(() => {536        expect(scaffold.plugins).not.to.be.called537      })538    })539    describe('forced', () => {540      let resetEnv541      beforeEach(function () {542        this.obj.isTextTerminal = true543        resetEnv = mockedEnv({544          CYPRESS_INTERNAL_FORCE_SCAFFOLD: '1',545        })546      })547      afterEach(() => {548        resetEnv()549      })550      it('calls scaffold when forced by environment variable', function () {551        return this.project.scaffold(this.obj).then(() => {552          expect(scaffold.integration).to.be.calledWith(this.obj.integrationFolder)553          expect(scaffold.fixture).to.be.calledWith(this.obj.fixturesFolder)554          expect(scaffold.support).to.be.calledWith(this.obj.supportFolder)555        })556      })557    })558    describe('not forced', () => {559      let resetEnv560      beforeEach(function () {561        this.obj.isTextTerminal = true562        resetEnv = mockedEnv({563          CYPRESS_INTERNAL_FORCE_SCAFFOLD: undefined,564        })565      })566      afterEach(() => {567        resetEnv()568      })569      it('does not scaffold integration folder', function () {570        return this.project.scaffold(this.obj).then(() => {571          expect(scaffold.integration).to.not.be.calledWith(this.obj.integrationFolder)572          expect(scaffold.fixture).to.not.be.calledWith(this.obj.fixturesFolder)573          // still scaffolds support folder due to old logic574          expect(scaffold.support).to.be.calledWith(this.obj.supportFolder)575        })576      })577    })578  })579  context('#watchSettings', () => {580    beforeEach(function () {581      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })582      this.project._server = { close () {}, startWebsockets () {} }583      sinon.stub(settings, 'pathToConfigFile').returns('/path/to/cypress.json')584      sinon.stub(settings, 'pathToCypressEnvJson').returns('/path/to/cypress.env.json')585      this.watch = sinon.stub(this.project.watchers, 'watch')586      this.watchTree = sinon.stub(this.project.watchers, 'watchTree')587    })588    it('watches cypress.json and cypress.env.json', function () {589      this.project.watchSettings({ onSettingsChanged () {} }, {})590      expect(this.watch).to.be.calledOnce591      expect(this.watchTree).to.be.calledOnce592      expect(this.watchTree).to.be.calledWith('/path/to/cypress.json')593      expect(this.watch).to.be.calledWith('/path/to/cypress.env.json')594    })595    it('sets onChange event when {changeEvents: true}', function (done) {596      this.project.watchSettings({ onSettingsChanged: () => done() }, {})597      // get the object passed to watchers.watch598      const obj = this.watch.getCall(0).args[1]599      expect(obj.onChange).to.be.a('function')600      obj.onChange()601    })602    it('does not call watch when {changeEvents: false}', function () {603      this.project.watchSettings({ onSettingsChanged: undefined }, {})604      expect(this.watch).not.to.be.called605    })606    it('does not call onSettingsChanged when generatedProjectIdTimestamp is less than 1 second', function () {607      let timestamp = new Date()608      this.project.generatedProjectIdTimestamp = timestamp609      const stub = sinon.stub()610      this.project.watchSettings({ onSettingsChanged: stub }, {})611      // get the object passed to watchers.watch612      const obj = this.watch.getCall(0).args[1]613      obj.onChange()614      expect(stub).not.to.be.called615      // subtract 1 second from our timestamp616      timestamp.setSeconds(timestamp.getSeconds() - 1)617      obj.onChange()618      expect(stub).to.be.calledOnce619    })620  })621  context('#watchPluginsFile', () => {622    beforeEach(function () {623      sinon.stub(fs, 'pathExists').resolves(true)624      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })625      this.project.watchers = { watchTree: sinon.spy() }626      sinon.stub(plugins, 'init').resolves()627      this.config = {628        pluginsFile: '/path/to/plugins-file',629      }630    })631    it('does nothing when {pluginsFile: false}', function () {632      this.config.pluginsFile = false633      return this.project.watchPluginsFile(this.config, {}).then(() => {634        expect(this.project.watchers.watchTree).not.to.be.called635      })636    })637    it('does nothing if pluginsFile does not exist', function () {638      fs.pathExists.resolves(false)639      return this.project.watchPluginsFile(this.config, {}).then(() => {640        expect(this.project.watchers.watchTree).not.to.be.called641      })642    })643    it('does nothing if in run mode', function () {644      return this.project.watchPluginsFile(this.config, {645        isTextTerminal: true,646      }).then(() => {647        expect(this.project.watchers.watchTree).not.to.be.called648      })649    })650    it('watches the pluginsFile', function () {651      return this.project.watchPluginsFile(this.config, {}).then(() => {652        expect(this.project.watchers.watchTree).to.be.calledWith(this.config.pluginsFile)653        expect(this.project.watchers.watchTree.lastCall.args[1]).to.be.an('object')654        expect(this.project.watchers.watchTree.lastCall.args[1].onChange).to.be.a('function')655      })656    })657    it('calls plugins.init when file changes', function () {658      return this.project.watchPluginsFile(this.config, {659        onError: () => {},660      }).then(() => {661        this.project.watchers.watchTree.firstCall.args[1].onChange()662        expect(plugins.init).to.be.calledWith(this.config)663      })664    })665    it('handles errors from calling plugins.init', function (done) {666      const error = { name: 'foo', message: 'foo' }667      plugins.init.rejects(error)668      this.project.watchPluginsFile(this.config, {669        onError (err) {670          expect(err).to.eql(error)671          done()672        },673      })674      .then(() => {675        this.project.watchers.watchTree.firstCall.args[1].onChange()676      })677    })678  })679  context('#startWebsockets', () => {680    beforeEach(function () {681      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })682      this.project.watchers = {}683      this.project._server = { close () {}, startWebsockets: sinon.stub() }684      sinon.stub(ProjectBase.prototype, 'open').resolves()685      sinon.stub(this.project, 'watchSettings')686    })687    it('calls server.startWebsockets with automation + config', async function () {688      const c = {}689      this.project.__setConfig(c)690      this.project.startWebsockets({}, c)691      const args = this.project.server.startWebsockets.lastCall.args692      expect(args[0]).to.be.an.instanceof(Automation)693      expect(args[1]).to.equal(c)694    })695    it('passes onReloadBrowser callback', function () {696      const fn = sinon.stub()697      this.project.server.startWebsockets.yieldsTo('onReloadBrowser')698      this.project.startWebsockets({ onReloadBrowser: fn }, {})699      expect(fn).to.be.calledOnce700    })701  })702  context('#getProjectId', () => {703    beforeEach(function () {704      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })705      this.verifyExistence = sinon.stub(ProjectBase.prototype, 'verifyExistence').resolves()706    })707    it('calls verifyExistence', function () {708      sinon.stub(settings, 'read').resolves({ projectId: 'id-123' })709      return this.project.getProjectId()710      .then(() => expect(this.verifyExistence).to.be.calledOnce)711    })712    it('returns the project id from settings', function () {713      sinon.stub(settings, 'read').resolves({ projectId: 'id-123' })714      return this.project.getProjectId()715      .then((id) => expect(id).to.eq('id-123'))716    })717    it('throws NO_PROJECT_ID with the projectRoot when no projectId was found', function () {718      sinon.stub(settings, 'read').resolves({})719      return this.project.getProjectId()720      .then((id) => {721        throw new Error('expected to fail, but did not')722      }).catch((err) => {723        expect(err.type).to.eq('NO_PROJECT_ID')724        expect(err.message).to.include('/_test-output/path/to/project-e2e')725      })726    })727    it('bubbles up Settings.read EACCES error', function () {728      const err = new Error()729      err.code = 'EACCES'730      sinon.stub(settings, 'read').rejects(err)731      return this.project.getProjectId()732      .then((id) => {733        throw new Error('expected to fail, but did not')734      }).catch((err) => {735        expect(err.code).to.eq('EACCES')736      })737    })738    it('bubbles up Settings.read EPERM error', function () {739      const err = new Error()740      err.code = 'EPERM'741      sinon.stub(settings, 'read').rejects(err)742      return this.project.getProjectId()743      .then((id) => {744        throw new Error('expected to fail, but did not')745      }).catch((err) => {746        expect(err.code).to.eq('EPERM')747      })748    })749  })750  context('#writeProjectId', () => {751    beforeEach(function () {752      this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })753      sinon.stub(settings, 'write')754      .withArgs(this.project.projectRoot, { projectId: 'id-123' })755      .resolves({ projectId: 'id-123' })756    })757    it('calls Settings.write with projectRoot and attrs', function () {758      return writeProjectId({ id: 'id-123' }).then((id) => {759        expect(id).to.eq('id-123')760      })761    })762    // TODO: This763    xit('sets generatedProjectIdTimestamp', function () {764      return writeProjectId({ id: 'id-123' }).then(() => {765        expect(this.project.generatedProjectIdTimestamp).to.be.a('date')766      })767    })768  })769  context('.add', () => {770    beforeEach(function () {771      this.pristinePath = Fixtures.projectPath('pristine')772    })773    it('inserts path into cache', function () {774      return add(this.pristinePath, {})775      .then(() => cache.read()).then((json) => {776        expect(json.PROJECTS).to.deep.eq([this.pristinePath])777      })778    })779    describe('if project at path has id', () => {780      it('returns object containing path and id', function () {781        sinon.stub(settings, 'read').resolves({ projectId: 'id-123' })782        return add(this.pristinePath, {})783        .then((project) => {784          expect(project.id).to.equal('id-123')785          expect(project.path).to.equal(this.pristinePath)786        })787      })788    })789    describe('if project at path does not have id', () => {790      it('returns object containing just the path', function () {791        sinon.stub(settings, 'read').rejects()792        return add(this.pristinePath, {})793        .then((project) => {794          expect(project.id).to.be.undefined795          expect(project.path).to.equal(this.pristinePath)796        })797      })798    })799    describe('if configFile is non-default', () => {800      it('doesn\'t cache anything and returns object containing just the path', function () {801        return add(this.pristinePath, { configFile: false })802        .then((project) => {803          expect(project.id).to.be.undefined804          expect(project.path).to.equal(this.pristinePath)805          return cache.read()806        }).then((json) => {807          expect(json.PROJECTS).to.deep.eq([])808        })809      })810    })811  })812  context('#createCiProject', () => {813    const projectRoot = '/_test-output/path/to/project-e2e'814    const configFile = 'cypress.config.js'815    beforeEach(function () {816      this.project = new ProjectBase({ projectRoot, testingType: 'e2e' })817      this.newProject = { id: 'project-id-123' }818      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')819      sinon.stub(settings, 'write').resolves()820      sinon.stub(commitInfo, 'getRemoteOrigin').resolves('remoteOrigin')821      sinon.stub(api, 'createProject')822      .withArgs({ foo: 'bar' }, 'remoteOrigin', 'auth-token-123')823      .resolves(this.newProject)824    })825    it('calls api.createProject with user session', function () {826      return createCiProject({ foo: 'bar', projectRoot }).then(() => {827        expect(api.createProject).to.be.calledWith({ foo: 'bar' }, 'remoteOrigin', 'auth-token-123')828      })829    })830    it('calls writeProjectId with id', function () {831      return createCiProject({ foo: 'bar', projectRoot, configFile }).then(() => {832        expect(settings.write).to.be.calledWith(projectRoot, { projectId: 'project-id-123' }, { configFile })833      })834    })835    it('returns project id', function () {836      return createCiProject({ foo: 'bar', projectRoot }).then((projectId) => {837        expect(projectId).to.eql(this.newProject)838      })839    })840  })841  context('#getRecordKeys', () => {842    beforeEach(function () {843      this.recordKeys = []844      this.project = new ProjectBase({ projectRoot: this.pristinePath, testingType: 'e2e' })845      sinon.stub(settings, 'read').resolves({ projectId: 'id-123' })846      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')847      sinon.stub(api, 'getProjectRecordKeys').resolves(this.recordKeys)848    })849    it('calls api.getProjectRecordKeys with project id + session', function () {850      return this.project.getRecordKeys().then(() => {851        expect(api.getProjectRecordKeys).to.be.calledWith('id-123', 'auth-token-123')852      })853    })854    it('returns ci keys', function () {855      return this.project.getRecordKeys().then((recordKeys) => {856        expect(recordKeys).to.equal(this.recordKeys)857      })858    })859  })860  context('#requestAccess', () => {861    beforeEach(function () {862      this.project = new ProjectBase({ projectRoot: this.pristinePath, testingType: 'e2e' })863      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')864      sinon.stub(api, 'requestAccess').resolves('response')865    })866    it('calls api.requestAccess with project id + auth token', function () {867      return this.project.requestAccess('project-id-123').then(() => {868        expect(api.requestAccess).to.be.calledWith('project-id-123', 'auth-token-123')869      })870    })871    it('returns response', function () {872      return this.project.requestAccess('project-id-123').then((response) => {873        expect(response).to.equal('response')874      })875    })876  })877  context('.remove', () => {878    beforeEach(() => {879      sinon.stub(cache, 'removeProject').resolves()880    })881    it('calls cache.removeProject with path', () => {882      return remove('/_test-output/path/to/project-e2e').then(() => {883        expect(cache.removeProject).to.be.calledWith('/_test-output/path/to/project-e2e')884      })885    })886  })887  context('.getId', () => {888    it('returns project id', function () {889      return getId(this.todosPath).then((id) => {890        expect(id).to.eq(this.projectId)891      })892    })893  })894  context('.getOrgs', () => {895    beforeEach(() => {896      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')897      sinon.stub(api, 'getOrgs').resolves([])898    })899    it('calls api.getOrgs', () => {900      return getOrgs().then((orgs) => {901        expect(orgs).to.deep.eq([])902        expect(api.getOrgs).to.be.calledOnce903        expect(api.getOrgs).to.be.calledWith('auth-token-123')904      })905    })906  })907  context('.paths', () => {908    beforeEach(() => {909      sinon.stub(cache, 'getProjectRoots').resolves([])910    })911    it('calls cache.getProjectRoots', () => {912      return paths().then((ret) => {913        expect(ret).to.deep.eq([])914        expect(cache.getProjectRoots).to.be.calledOnce915      })916    })917  })918  context('.getPathsAndIds', () => {919    beforeEach(() => {920      sinon.stub(cache, 'getProjectRoots').resolves([921        '/path/to/first',922        '/path/to/second',923      ])924      sinon.stub(settings, 'id').resolves('id-123')925    })926    it('returns array of objects with paths and ids', () => {927      return getPathsAndIds().then((pathsAndIds) => {928        expect(pathsAndIds).to.eql([929          {930            path: '/path/to/first',931            id: 'id-123',932          },933          {934            path: '/path/to/second',935            id: 'id-123',936          },937        ])938      })939    })940  })941  context('.getProjectStatuses', () => {942    beforeEach(() => {943      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')944    })945    it('gets projects from api', () => {946      sinon.stub(api, 'getProjects').resolves([])947      return getProjectStatuses([])948      .then(() => {949        expect(api.getProjects).to.have.been.calledWith('auth-token-123')950      })951    })952    it('returns array of projects', () => {953      sinon.stub(api, 'getProjects').resolves([])954      return getProjectStatuses([])955      .then((projectsWithStatuses) => {956        expect(projectsWithStatuses).to.eql([])957      })958    })959    it('returns same number as client projects, even if there are less api projects', () => {960      sinon.stub(api, 'getProjects').resolves([])961      return getProjectStatuses([{}])962      .then((projectsWithStatuses) => {963        expect(projectsWithStatuses.length).to.eql(1)964      })965    })966    it('returns same number as client projects, even if there are more api projects', () => {967      sinon.stub(api, 'getProjects').resolves([{}, {}])968      return getProjectStatuses([{}])969      .then((projectsWithStatuses) => {970        expect(projectsWithStatuses.length).to.eql(1)971      })972    })973    it('merges in details of matching projects', () => {974      sinon.stub(api, 'getProjects').resolves([975        { id: 'id-123', lastBuildStatus: 'passing' },976      ])977      return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])978      .then((projectsWithStatuses) => {979        expect(projectsWithStatuses[0]).to.eql({980          id: 'id-123',981          path: '/_test-output/path/to/project',982          lastBuildStatus: 'passing',983          state: 'VALID',984        })985      })986    })987    it('returns client project when it has no id', () => {988      sinon.stub(api, 'getProjects').resolves([])989      return getProjectStatuses([{ path: '/_test-output/path/to/project' }])990      .then((projectsWithStatuses) => {991        expect(projectsWithStatuses[0]).to.eql({992          path: '/_test-output/path/to/project',993          state: 'VALID',994        })995      })996    })997    describe('when client project has id and there is no matching user project', () => {998      beforeEach(() => {999        sinon.stub(api, 'getProjects').resolves([])1000      })1001      it('marks project as invalid if api 404s', () => {1002        sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 404 })1003        return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])1004        .then((projectsWithStatuses) => {1005          expect(projectsWithStatuses[0]).to.eql({1006            id: 'id-123',1007            path: '/_test-output/path/to/project',1008            state: 'INVALID',1009          })1010        })1011      })1012      it('marks project as unauthorized if api 403s', () => {1013        sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 403 })1014        return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])1015        .then((projectsWithStatuses) => {1016          expect(projectsWithStatuses[0]).to.eql({1017            id: 'id-123',1018            path: '/_test-output/path/to/project',1019            state: 'UNAUTHORIZED',1020          })1021        })1022      })1023      it('merges in project details and marks valid if somehow project exists and is authorized', () => {1024        sinon.stub(api, 'getProject').resolves({ id: 'id-123', lastBuildStatus: 'passing' })1025        return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])1026        .then((projectsWithStatuses) => {1027          expect(projectsWithStatuses[0]).to.eql({1028            id: 'id-123',1029            path: '/_test-output/path/to/project',1030            lastBuildStatus: 'passing',1031            state: 'VALID',1032          })1033        })1034      })1035      it('throws error if not accounted for', () => {1036        const error = { name: '', message: '' }1037        sinon.stub(api, 'getProject').rejects(error)1038        return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])1039        .then(() => {1040          throw new Error('should have caught error but did not')1041        }).catch((err) => {1042          expect(err).to.equal(error)1043        })1044      })1045    })1046  })1047  context('.getProjectStatus', () => {1048    beforeEach(function () {1049      this.clientProject = {1050        id: 'id-123',1051        path: '/_test-output/path/to/project',1052      }1053      sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')1054    })1055    it('gets project from api', function () {1056      sinon.stub(api, 'getProject').resolves([])1057      return getProjectStatus(this.clientProject)1058      .then(() => {1059        expect(api.getProject).to.have.been.calledWith('id-123', 'auth-token-123')1060      })1061    })1062    it('returns project merged with details', function () {1063      sinon.stub(api, 'getProject').resolves({1064        lastBuildStatus: 'passing',1065      })1066      return getProjectStatus(this.clientProject)1067      .then((project) => {1068        expect(project).to.eql({1069          id: 'id-123',1070          path: '/_test-output/path/to/project',1071          lastBuildStatus: 'passing',1072          state: 'VALID',1073        })1074      })1075    })1076    it('returns project, marked as valid, if it does not have an id, without querying api', function () {1077      sinon.stub(api, 'getProject')1078      this.clientProject.id = undefined1079      return getProjectStatus(this.clientProject)1080      .then((project) => {1081        expect(project).to.eql({1082          id: undefined,1083          path: '/_test-output/path/to/project',1084          state: 'VALID',1085        })1086        expect(api.getProject).not.to.be.called1087      })1088    })1089    it('marks project as invalid if api 404s', function () {1090      sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 404 })1091      return getProjectStatus(this.clientProject)1092      .then((project) => {1093        expect(project).to.eql({1094          id: 'id-123',1095          path: '/_test-output/path/to/project',1096          state: 'INVALID',1097        })1098      })1099    })1100    it('marks project as unauthorized if api 403s', function () {1101      sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 403 })1102      return getProjectStatus(this.clientProject)1103      .then((project) => {1104        expect(project).to.eql({1105          id: 'id-123',1106          path: '/_test-output/path/to/project',1107          state: 'UNAUTHORIZED',1108        })1109      })1110    })1111    it('throws error if not accounted for', function () {1112      const error = { name: '', message: '' }1113      sinon.stub(api, 'getProject').rejects(error)1114      return getProjectStatus(this.clientProject)1115      .then(() => {1116        throw new Error('should have caught error but did not')1117      }).catch((err) => {1118        expect(err).to.equal(error)1119      })1120    })1121  })...

Full Screen

Full Screen

project-base.js

Source:project-base.js Github

copy

Full Screen

...295            debug('plugin config yielded: %o', modifiedCfg);296            return config.updateWithPluginValues(cfg, modifiedCfg);297        });298    }299    startCtDevServer(specs, config) {300        return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {301            // CT uses a dev-server to build the bundle.302            // We start the dev server here.303            const devServerOptions = yield dev_server_1.default.start({ specs, config });304            if (!devServerOptions) {305                throw new Error([306                    'It looks like nothing was returned from on(\'dev-server:start\', {here}).',307                    'Make sure that the dev-server:start function returns an object.',308                    'For example: on("dev-server:start", () => startWebpackDevServer({ webpackConfig }))',309                ].join('\n'));310            }311            return { port: devServerOptions.port };312        });313    }314    initSpecStore({ specs, config, }) {315        return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {316            const specsStore = new specs_store_1.SpecsStore(config, this.testingType);317            const startSpecWatcher = () => {318                return specsStore.watch({319                    onSpecsChanged: (specs) => {320                        // both e2e and CT watch the specs and send them to the321                        // client to be shown in the SpecList.322                        this.server.sendSpecList(specs, this.testingType);323                        if (this.testingType === 'component') {324                            // ct uses the dev-server to build and bundle the speces.325                            // send new files to dev server326                            dev_server_1.default.updateSpecs(specs);327                        }328                    },329                });330            };331            let ctDevServerPort;332            if (this.testingType === 'component') {333                const { port } = yield this.startCtDevServer(specs, config);334                ctDevServerPort = port;335            }336            return specsStore.storeSpecFiles()337                .return({338                specsStore,339                ctDevServerPort,340                startSpecWatcher,341            });342        });343    }344    watchPluginsFile(cfg, options) {345        return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () {346            debug(`attempt watch plugins file: ${cfg.pluginsFile}`);347            if (!cfg.pluginsFile || options.isTextTerminal) {...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1describe('Cypress Test', () => {2  it('Test', () => {3    cy.startCtDevServer();4  });5});6const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');7module.exports = (on, config) => {8  on('file:preprocessor', cypressTypeScriptPreprocessor);9  on('task', {10    startCtDevServer: require('@cypress/webpack-dev-server/task'),11  });12};13const wp = require('@cypress/webpack-preprocessor');14module.exports = (on, config) => {15  const options = {16    webpackOptions: require('../../webpack.config'),17  };18  on('file:preprocessor', wp(options));19};20{21}22const path = require('path');23const webpack = require('webpack');24module.exports = {25  module: {26      {27        test: /\.(js|jsx|ts|tsx)$/,28        include: path.resolve(__dirname, 'src'),29        use: {30        },31      },32  },33  resolve: {34  },35  output: {36    path: path.resolve(__dirname, 'dist'),37  },38  plugins: [new webpack.HotModuleReplacementPlugin()],39  devServer: {40  },41};42{43  "compilerOptions": {

Full Screen

Using AI Code Generation

copy

Full Screen

1import { startCtDevServer } from "@cypress/webpack-dev-server";2import webpackConfig from "../webpack.config";3import webpackDevServerConfig from "../webpack-dev-server.config";4import cypressConfig from "../cypress.json";5import config from "../config";6import cypressConfigFile from "../cypress.json";7function startDevServer() {8  return startCtDevServer({9  });10}11function runTests() {12  return cy.runSpecs({13  });14}15function runTestsAndStartDevServer() {16  return runTests().then(() => {17    startDevServer();18  });19}20module.exports = runTestsAndStartDevServer;

Full Screen

Using AI Code Generation

copy

Full Screen

1before(() => {2  startCtDevServer();3});4after(() => {5  stopCtDevServer();6});7{8  "env": {9  }10}11{12  "scripts": {13  }14}15const { startDevServer } = require("@cypress/webpack-dev-server");16module.exports = (on, config) => {17  on("dev-server:start", (options) => {18    return startDevServer({19    });20  });21  return config;22};23import { startDevServer } from "@cypress/webpack-dev-server";24import webpackConfig from "../../webpack.config";25const startDevServer = (on, config) => {26  on("dev-server:start", (options) => {27    return startDevServer({28    });29  });30  return config;31};32import webpackConfig from "../../webpack.config";33startDevServer(on, config);34import { startDevServer } from "@cypress/webpack-dev-server";35import webpackConfig from "../../webpack.config";36const startDevServer = (on, config) => {37  on("dev-server:start", (options

Full Screen

Using AI Code Generation

copy

Full Screen

1Cypress.startCtDevServer({2  cypressConfig: {3  },4  webpackConfig: {5    module: {6        {7        },8    },9    resolve: {10    },11  },12  webpackDevServerConfig: {13    watchOptions: {14    },15  },16  tsConfig: {17    compilerOptions: {

Full Screen

Using AI Code Generation

copy

Full Screen

1startCtDevServer({2  env: {3  }4})5const webpackConfig = require('@vue/cli-service/webpack.config.js')6const webpackDevMiddleware = require('webpack-dev-middleware')7const webpack = require('webpack')8module.exports = (on, config) => {9  on('dev-server:start', (options) => {10    return new Promise((resolve, reject) => {11      const compiler = webpack(webpackConfig)12      const middleware = webpackDevMiddleware(compiler, {13      })14      return resolve({15      })16    })17  })18}

Full Screen

Using AI Code Generation

copy

Full Screen

1import { startDevServer } from '@cypress/vite-dev-server'2startDevServer({3  proxy: {},4  headers: {},5  serve: {},6  watch: {},7  define: {},8  alias: {},9  resolve: {},

Full Screen

Using AI Code Generation

copy

Full Screen

1## [Usage](#usage)2### [Configuration](#configuration)3{4  "env": {5    "ctDevServerConfig": {6      "staticOptions": {}7    }8  }9}10#### [ctDevServerConfig](#ctDevServerConfig)11- **`staticOptions`** `object` - Options object to pass to [serve-static](

Full Screen

Cypress Tutorial

Cypress is a renowned Javascript-based open-source, easy-to-use end-to-end testing framework primarily used for testing web applications. Cypress is a relatively new player in the automation testing space and has been gaining much traction lately, as evidenced by the number of Forks (2.7K) and Stars (42.1K) for the project. LambdaTest’s Cypress Tutorial covers step-by-step guides that will help you learn from the basics till you run automation tests on LambdaTest.

Chapters:

  1. What is Cypress? -
  2. Why Cypress? - Learn why Cypress might be a good choice for testing your web applications.
  3. Features of Cypress Testing - Learn about features that make Cypress a powerful and flexible tool for testing web applications.
  4. Cypress Drawbacks - Although Cypress has many strengths, it has a few limitations that you should be aware of.
  5. Cypress Architecture - Learn more about Cypress architecture and how it is designed to be run directly in the browser, i.e., it does not have any additional servers.
  6. Browsers Supported by Cypress - Cypress is built on top of the Electron browser, supporting all modern web browsers. Learn browsers that support Cypress.
  7. Selenium vs Cypress: A Detailed Comparison - Compare and explore some key differences in terms of their design and features.
  8. Cypress Learning: Best Practices - Take a deep dive into some of the best practices you should use to avoid anti-patterns in your automation tests.
  9. How To Run Cypress Tests on LambdaTest? - Set up a LambdaTest account, and now you are all set to learn how to run Cypress tests.

Certification

You can elevate your expertise with end-to-end testing using the Cypress automation framework and stay one step ahead in your career by earning a Cypress certification. Check out our Cypress 101 Certification.

YouTube

Watch this 3 hours of complete tutorial to learn the basics of Cypress and various Cypress commands with the Cypress testing at LambdaTest.

Run Cypress automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful