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