mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-11 01:29:59 -06:00
Merge branch 'develop' into feature/simulated-top-cookie-handling
This commit is contained in:
@@ -106,7 +106,7 @@ generates:
|
||||
<<: *vueOperations
|
||||
|
||||
'./packages/frontend-shared/src/generated/graphql.ts':
|
||||
documents: './packages/frontend-shared/src/{gql-components,graphql}/**/*.{vue,ts,tsx,js,jsx}'
|
||||
documents: './packages/frontend-shared/src/{gql-components,graphql,composables}/**/*.{vue,ts,tsx,js,jsx}'
|
||||
<<: *vueOperations
|
||||
###
|
||||
# All GraphQL documents imported into the .spec.tsx files for component testing.
|
||||
|
||||
@@ -94,6 +94,11 @@ export type MountResponse<T> = {
|
||||
component: T
|
||||
};
|
||||
|
||||
// 'zone.js/testing' is not properly aliasing `it.skip` but it does provide `xit`/`xspecify`
|
||||
// Written up under https://github.com/angular/angular/issues/46297 but is not seeing movement
|
||||
// so we'll patch here pending a fix in that library
|
||||
globalThis.it.skip = globalThis.xit
|
||||
|
||||
/**
|
||||
* Bootstraps the TestModuleMetaData passed to the TestBed
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ exports['makeWebpackConfig ignores userland webpack `output.publicPath` and `dev
|
||||
},
|
||||
"mode": "development",
|
||||
"optimization": {
|
||||
"emitOnErrors": true,
|
||||
"splitChunks": {
|
||||
"chunks": "all"
|
||||
}
|
||||
@@ -33,6 +34,7 @@ exports['makeWebpackConfig ignores userland webpack `output.publicPath` and `dev
|
||||
},
|
||||
"mode": "development",
|
||||
"optimization": {
|
||||
"noEmitOnErrors": false,
|
||||
"splitChunks": {
|
||||
"chunks": "all"
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ for (const project of WEBPACK_REACT) {
|
||||
it('should mount a passing test', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('app.component.cy.ts').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
cy.get('li.command').first().within(() => {
|
||||
cy.get('.command-method').should('contain', 'mount')
|
||||
cy.get('.command-message').should('contain', 'AppComponent')
|
||||
@@ -33,8 +33,7 @@ for (const project of WEBPACK_REACT) {
|
||||
it('should live-reload on src changes', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('app.component.cy.ts').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
@@ -43,7 +42,7 @@ for (const project of WEBPACK_REACT) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.failed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
@@ -52,7 +51,28 @@ for (const project of WEBPACK_REACT) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
it('should show compilation errors on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('app.component.cy.ts').click()
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
// Create compilation error
|
||||
cy.withCtx(async (ctx) => {
|
||||
const componentFilePath = ctx.path.join('src', 'app', 'app.component.ts')
|
||||
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
componentFilePath,
|
||||
(await ctx.file.readFileInProject(componentFilePath)).replace('class', 'classaaaaa'),
|
||||
)
|
||||
})
|
||||
|
||||
// The test should fail and the stack trace should appear in the command log
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
cy.contains('The following error originated from your test code, not from Cypress.').should('exist')
|
||||
})
|
||||
|
||||
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23455
|
||||
@@ -67,8 +87,7 @@ for (const project of WEBPACK_REACT) {
|
||||
})
|
||||
|
||||
cy.contains('new.component.cy.ts').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,16 +21,14 @@ for (const project of WEBPACK_REACT) {
|
||||
it('should mount a passing test', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('App.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
it('should live-reload on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('App.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
@@ -39,7 +37,7 @@ for (const project of WEBPACK_REACT) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.failed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
@@ -48,7 +46,25 @@ for (const project of WEBPACK_REACT) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
it('should show compilation errors on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('App.cy.js').click()
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
ctx.path.join('src', 'App.js'),
|
||||
(await ctx.file.readFileInProject(ctx.path.join('src', 'App.js'))).replace('export', 'expart'),
|
||||
)
|
||||
})
|
||||
|
||||
// The test should fail and the stack trace should appear in the command log
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
cy.contains('The following error originated from your test code, not from Cypress.').should('exist')
|
||||
})
|
||||
|
||||
it('should detect new spec', () => {
|
||||
@@ -62,8 +78,7 @@ for (const project of WEBPACK_REACT) {
|
||||
})
|
||||
|
||||
cy.contains('New.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,16 +21,14 @@ for (const project of WEBPACK_REACT) {
|
||||
it('should mount a passing test', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('index.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
it('should live-reload on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('index.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
const indexPath = ctx.path.join('pages', 'index.js')
|
||||
@@ -41,7 +39,7 @@ for (const project of WEBPACK_REACT) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.failed > .num', { timeout: 10000 }).should('contain', 1)
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
const indexTestPath = ctx.path.join('pages', 'index.cy.js')
|
||||
@@ -52,7 +50,28 @@ for (const project of WEBPACK_REACT) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
it('should show compilation errors on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('index.cy.js').click()
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
// Create compilation error
|
||||
cy.withCtx(async (ctx) => {
|
||||
const indexPath = ctx.path.join('pages', 'index.js')
|
||||
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
indexPath,
|
||||
(await ctx.file.readFileInProject(indexPath)).replace('export', 'expart'),
|
||||
)
|
||||
})
|
||||
|
||||
// The test should fail and the stack trace should appear in the command log
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
cy.contains('The following error originated from your test code, not from Cypress.').should('exist')
|
||||
})
|
||||
|
||||
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23417
|
||||
@@ -70,16 +89,14 @@ for (const project of WEBPACK_REACT) {
|
||||
})
|
||||
|
||||
cy.contains('New.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23417
|
||||
it.skip('should allow import of global styles in support file', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('styles.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ for (const project of PROJECTS) {
|
||||
it('should mount a passing test and live-reload', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('Tutorial.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
const tutorialVuePath = ctx.path.join('components', 'Tutorial.vue')
|
||||
@@ -35,7 +34,7 @@ for (const project of PROJECTS) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.failed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
const tutorialCyPath = ctx.path.join('components', 'Tutorial.cy.js')
|
||||
@@ -46,7 +45,28 @@ for (const project of PROJECTS) {
|
||||
)
|
||||
})
|
||||
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
|
||||
it('should show compilation errors on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('Tutorial.cy.js').click()
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
// Create compilation error
|
||||
cy.withCtx(async (ctx) => {
|
||||
const tutorialVuePath = ctx.path.join('components', 'Tutorial.vue')
|
||||
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
tutorialVuePath,
|
||||
(await ctx.file.readFileInProject(tutorialVuePath)).replace('export', 'expart'),
|
||||
)
|
||||
})
|
||||
|
||||
// The test should fail and the stack trace should appear in the command log
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
cy.contains('The following error originated from your test code, not from Cypress.').should('exist')
|
||||
})
|
||||
|
||||
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23455
|
||||
@@ -64,8 +84,7 @@ for (const project of PROJECTS) {
|
||||
})
|
||||
|
||||
cy.contains('New.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,12 +21,32 @@ for (const project of PROJECTS) {
|
||||
it('should mount a passing test', () => {
|
||||
cy.visitApp()
|
||||
cy.contains('HelloWorld.cy.js').click()
|
||||
cy.waitForSpecToFinish()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
cy.get('.commands-container').within(() => {
|
||||
cy.contains('mount')
|
||||
cy.contains('<HelloWorld ... />')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show compilation errors on src changes', () => {
|
||||
cy.visitApp()
|
||||
|
||||
cy.contains('HelloWorld.cy.js').click()
|
||||
cy.waitForSpecToFinish({ passCount: 1 })
|
||||
|
||||
// Create compilation error
|
||||
cy.withCtx(async (ctx) => {
|
||||
const helloWorldVuePath = ctx.path.join('src', 'components', 'HelloWorld.vue')
|
||||
|
||||
await ctx.actions.file.writeFileInProject(
|
||||
helloWorldVuePath,
|
||||
(await ctx.file.readFileInProject(helloWorldVuePath)).replace('export', 'expart'),
|
||||
)
|
||||
})
|
||||
|
||||
// The test should fail and the stack trace should appear in the command log
|
||||
cy.waitForSpecToFinish({ failCount: 1 })
|
||||
cy.contains('The following error originated from your test code, not from Cypress.').should('exist')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
import type { ExpectedResults } from '@packages/app/cypress/e2e/support/execute-spec'
|
||||
import { waitForSpecToFinish } from '@packages/app/cypress/e2e/support/execute-spec'
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
@@ -11,24 +13,9 @@ declare global {
|
||||
* 3. Waits (with a timeout of 30s) for the Rerun all tests button to be present. This ensures all tests have completed
|
||||
*
|
||||
*/
|
||||
waitForSpecToFinish()
|
||||
waitForSpecToFinish(expectedResults?: ExpectedResults): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here we export the function with no intention to import it
|
||||
// This only tells the typescript type checker that this definitely is a module
|
||||
// This way, we are allowed to use the global namespace declaration
|
||||
export const waitForSpecToFinish = () => {
|
||||
// First ensure the test is loaded
|
||||
cy.get('.passed > .num').should('contain', '--')
|
||||
cy.get('.failed > .num').should('contain', '--')
|
||||
|
||||
// Then ensure the tests are running
|
||||
cy.contains('Your tests are loading...').should('not.exist')
|
||||
|
||||
// Then ensure the tests have finished
|
||||
cy.get('[aria-label="Rerun all tests"]', { timeout: 30000 })
|
||||
}
|
||||
|
||||
Cypress.Commands.add('waitForSpecToFinish', waitForSpecToFinish)
|
||||
|
||||
@@ -24,9 +24,18 @@ export function makeDefaultWebpackConfig (
|
||||
|
||||
debug(`Using HtmlWebpackPlugin version ${version} from ${importPath}`)
|
||||
|
||||
const optimization = <Record<string, any>>{}
|
||||
|
||||
if (config.sourceWebpackModulesResult.webpack.majorVersion === 5) {
|
||||
optimization.emitOnErrors = true
|
||||
} else {
|
||||
optimization.noEmitOnErrors = false
|
||||
}
|
||||
|
||||
const finalConfig = {
|
||||
mode: 'development',
|
||||
optimization: {
|
||||
...optimization,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect } from 'chai'
|
||||
import EventEmitter from 'events'
|
||||
import snapshot from 'snap-shot-it'
|
||||
import { WebpackDevServerConfig } from '../src/devServer'
|
||||
import { sourceDefaultWebpackDependencies } from '../src/helpers/sourceRelativeWebpackModules'
|
||||
import { CYPRESS_WEBPACK_ENTRYPOINT, makeWebpackConfig } from '../src/makeWebpackConfig'
|
||||
import { createModuleMatrixResult } from './test-helpers/createModuleMatrixResult'
|
||||
|
||||
@@ -24,12 +23,18 @@ describe('makeWebpackConfig', () => {
|
||||
progress: true,
|
||||
overlay: true, // This will be overridden by makeWebpackConfig.ts
|
||||
},
|
||||
optimization: {
|
||||
noEmitOnErrors: true, // This will be overridden by makeWebpackConfig.ts
|
||||
},
|
||||
},
|
||||
devServerEvents: new EventEmitter(),
|
||||
}
|
||||
const actual = await makeWebpackConfig({
|
||||
devServerConfig,
|
||||
sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig),
|
||||
sourceWebpackModulesResult: createModuleMatrixResult({
|
||||
webpack: 4,
|
||||
webpackDevServer: 3,
|
||||
}),
|
||||
})
|
||||
|
||||
// plugins contain circular deps which cannot be serialized in a snapshot.
|
||||
@@ -64,13 +69,16 @@ describe('makeWebpackConfig', () => {
|
||||
overlay: true, // This will be overridden by makeWebpackConfig.ts
|
||||
},
|
||||
},
|
||||
optimization: {
|
||||
emitOnErrors: false, // This will be overridden by makeWebpackConfig.ts
|
||||
},
|
||||
},
|
||||
devServerEvents: new EventEmitter(),
|
||||
}
|
||||
const actual = await makeWebpackConfig({
|
||||
devServerConfig,
|
||||
sourceWebpackModulesResult: createModuleMatrixResult({
|
||||
webpack: 4,
|
||||
webpack: 5,
|
||||
webpackDevServer: 4,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -202,6 +202,8 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
|
||||
// we need to set entry and output
|
||||
entry,
|
||||
output: {
|
||||
// disable automatic publicPath
|
||||
publicPath: '',
|
||||
path: path.dirname(outputPath),
|
||||
filename: path.basename(outputPath),
|
||||
},
|
||||
@@ -220,6 +222,10 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
|
||||
|
||||
// override typescript to always generate proper source maps
|
||||
overrideSourceMaps(true, options.typescript)
|
||||
|
||||
// To support dynamic imports, we have to disable any code splitting.
|
||||
debug('Limiting number of chunks to 1')
|
||||
opts.plugins = (opts.plugins || []).concat(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }))
|
||||
})
|
||||
.value() as any
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ const expect = chai.expect
|
||||
chai.use(require('sinon-chai'))
|
||||
|
||||
const webpack = sinon.stub()
|
||||
const LimitChunkCountPluginStub = sinon.stub()
|
||||
|
||||
webpack.optimize = {
|
||||
LimitChunkCountPlugin: LimitChunkCountPluginStub,
|
||||
}
|
||||
|
||||
mockery.enable({
|
||||
warnOnUnregistered: false,
|
||||
@@ -149,6 +154,7 @@ describe('webpack preprocessor', function () {
|
||||
|
||||
return this.run().then(() => {
|
||||
expect(webpack.lastCall.args[0].output).to.eql({
|
||||
publicPath: '',
|
||||
path: 'output',
|
||||
filename: 'output.ts.js',
|
||||
})
|
||||
|
||||
@@ -611,7 +611,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
cy.get('[data-cy="copy-button"]').click()
|
||||
cy.contains('Copied!')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('npx cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -624,7 +624,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
cy.get('[data-cy="copy-button"]').click()
|
||||
cy.contains('Copied!')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('npx cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { shouldHaveTestResults } from '../runner/support/spec-loader'
|
||||
|
||||
export interface ExpectedResults
|
||||
{
|
||||
passCount?: number
|
||||
failCount?: number
|
||||
pendingCount?: number
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
@@ -11,11 +18,7 @@ declare global {
|
||||
* 3. Waits (with a timeout of 30s) for the Rerun all tests button to be present. This ensures all tests have completed
|
||||
*
|
||||
*/
|
||||
waitForSpecToFinish(expectedResults?: {
|
||||
passCount?: number
|
||||
failCount?: number
|
||||
pendingCount?: number
|
||||
}): void
|
||||
waitForSpecToFinish(expectedResults?: ExpectedResults): void
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +32,7 @@ export const waitForSpecToFinish = (expectedResults) => {
|
||||
cy.get('.failed > .num').should('contain', '--')
|
||||
|
||||
// Then ensure the tests are running
|
||||
cy.contains('Your tests are loading...', { timeout: 10000 }).should('not.exist')
|
||||
cy.contains('Your tests are loading...', { timeout: 20000 }).should('not.exist')
|
||||
|
||||
// Then ensure the tests have finished
|
||||
cy.get('[aria-label="Rerun all tests"]', { timeout: 30000 })
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Bluebird from 'bluebird'
|
||||
import { EventEmitter } from 'events'
|
||||
import type { MobxRunnerStore } from '@packages/app/src/store/mobx-runner-store'
|
||||
import type { RunState } from '@packages/types/src/driver'
|
||||
import type MobX from 'mobx'
|
||||
import type { LocalBusEmitsMap, LocalBusEventMap, DriverToLocalBus, SocketToDriverMap } from './event-manager-types'
|
||||
import type { AutomationElementId, FileDetails } from '@packages/types'
|
||||
|
||||
import type { RunState, CachedTestState, AutomationElementId, FileDetails, ReporterStartInfo, ReporterRunState } from '@packages/types'
|
||||
|
||||
import { logger } from './logger'
|
||||
import type { Socket } from '@packages/socket/lib/browser'
|
||||
@@ -34,7 +34,7 @@ interface AddGlobalListenerOptions {
|
||||
randomString: string
|
||||
}
|
||||
|
||||
const driverToReporterEvents = 'paused session:add'.split(' ')
|
||||
const driverToReporterEvents = 'paused'.split(' ')
|
||||
const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
|
||||
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
|
||||
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
|
||||
@@ -87,8 +87,7 @@ export class EventManager {
|
||||
|
||||
const rerun = () => {
|
||||
if (!this) {
|
||||
// if the tests have been reloaded
|
||||
// then there is nothing to rerun
|
||||
// if the tests have been reloaded then there is nothing to rerun
|
||||
return
|
||||
}
|
||||
|
||||
@@ -249,10 +248,10 @@ export class EventManager {
|
||||
this.saveState(state)
|
||||
})
|
||||
|
||||
this.reporterBus.on('clear:session', () => {
|
||||
this.reporterBus.on('clear:all:sessions', () => {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.backend('clear:session')
|
||||
Cypress.backend('clear:sessions', true)
|
||||
.then(rerun)
|
||||
})
|
||||
|
||||
@@ -336,8 +335,8 @@ export class EventManager {
|
||||
// @ts-ignore
|
||||
const $window = this.$CypressDriver.$(window)
|
||||
|
||||
// This is a test-only even. It's used to
|
||||
// trigger a re-reun for the drive rerun.cy.js spec.
|
||||
// This is a test-only event. It's used to
|
||||
// trigger a rerun for the driver rerun.cy.js spec.
|
||||
$window.on('test:trigger:rerun', rerun)
|
||||
|
||||
// when we actually unload then
|
||||
@@ -395,9 +394,9 @@ export class EventManager {
|
||||
return Cypress.initialize({
|
||||
$autIframe,
|
||||
onSpecReady: () => {
|
||||
// get the current runnable in case we reran mid-test due to a visit
|
||||
// to a new domain
|
||||
this.ws.emit('get:existing:run:state', (state: RunState = {}) => {
|
||||
// get the current runnable states and cached test state
|
||||
// in case we reran mid-test due to a visit to a new domain
|
||||
this.ws.emit('get:cached:test:state', (runState: RunState = {}, testState: CachedTestState) => {
|
||||
if (!Cypress.runner) {
|
||||
// the tests have been reloaded
|
||||
return
|
||||
@@ -405,40 +404,40 @@ export class EventManager {
|
||||
|
||||
const hideCommandLog = window.__CYPRESS_CONFIG__.hideCommandLog
|
||||
|
||||
this.studioStore.initialize(config, state)
|
||||
this.studioStore.initialize(config, runState)
|
||||
|
||||
const runnables = Cypress.runner.normalizeAll(state.tests, hideCommandLog)
|
||||
const runnables = Cypress.runner.normalizeAll(runState.tests, hideCommandLog)
|
||||
|
||||
const run = () => {
|
||||
performance.mark('initialize-end')
|
||||
performance.measure('initialize', 'initialize-start', 'initialize-end')
|
||||
|
||||
this._runDriver(state)
|
||||
this._runDriver(runState, testState)
|
||||
}
|
||||
|
||||
if (!hideCommandLog) {
|
||||
this.reporterBus.emit('runnables:ready', runnables)
|
||||
}
|
||||
|
||||
if (state?.numLogs) {
|
||||
Cypress.runner.setNumLogs(state.numLogs)
|
||||
if (runState?.numLogs) {
|
||||
Cypress.runner.setNumLogs(runState.numLogs)
|
||||
}
|
||||
|
||||
if (state.startTime) {
|
||||
Cypress.runner.setStartTime(state.startTime)
|
||||
if (runState.startTime) {
|
||||
Cypress.runner.setStartTime(runState.startTime)
|
||||
}
|
||||
|
||||
if (config.isTextTerminal && !state.currentId) {
|
||||
if (config.isTextTerminal && !runState.currentId) {
|
||||
// we are in run mode and it's the first load
|
||||
// store runnables in backend and maybe send to dashboard
|
||||
return this.ws.emit('set:runnables:and:maybe:record:tests', runnables, run)
|
||||
}
|
||||
|
||||
if (state.currentId) {
|
||||
if (runState.currentId) {
|
||||
// if we have a currentId it means
|
||||
// we need to tell the Cypress to skip
|
||||
// ahead to that test
|
||||
Cypress.runner.resumeAtTest(state.currentId, state.emissions)
|
||||
Cypress.runner.resumeAtTest(runState.currentId, runState.emissions)
|
||||
}
|
||||
|
||||
return run()
|
||||
@@ -464,7 +463,7 @@ export class EventManager {
|
||||
}
|
||||
|
||||
return new Bluebird((resolve) => {
|
||||
this.reporterBus.emit('reporter:collect:run:state', (reporterState) => {
|
||||
this.reporterBus.emit('reporter:collect:run:state', (reporterState: ReporterRunState) => {
|
||||
resolve({
|
||||
...reporterState,
|
||||
studio: {
|
||||
@@ -749,9 +748,9 @@ export class EventManager {
|
||||
window.top.addEventListener('message', crossOriginOnMessageRef, false)
|
||||
}
|
||||
|
||||
_runDriver (state) {
|
||||
_runDriver (runState: RunState, testState: CachedTestState) {
|
||||
performance.mark('run-s')
|
||||
Cypress.run(() => {
|
||||
Cypress.run(testState, () => {
|
||||
performance.mark('run-e')
|
||||
performance.measure('run', 'run-s', 'run-e')
|
||||
})
|
||||
@@ -760,14 +759,14 @@ export class EventManager {
|
||||
|
||||
this.reporterBus.emit('reporter:start', {
|
||||
startTime: Cypress.runner.getStartTime(),
|
||||
numPassed: state.passed,
|
||||
numFailed: state.failed,
|
||||
numPending: state.pending,
|
||||
autoScrollingEnabled: state.autoScrollingEnabled,
|
||||
isSpecsListOpen: state.isSpecsListOpen,
|
||||
scrollTop: state.scrollTop,
|
||||
numPassed: runState.passed,
|
||||
numFailed: runState.failed,
|
||||
numPending: runState.pending,
|
||||
autoScrollingEnabled: runState.autoScrollingEnabled,
|
||||
isSpecsListOpen: runState.isSpecsListOpen,
|
||||
scrollTop: runState.scrollTop,
|
||||
studioActive: hasRunnableId,
|
||||
})
|
||||
} as ReporterStartInfo)
|
||||
}
|
||||
|
||||
stop () {
|
||||
@@ -783,8 +782,8 @@ export class EventManager {
|
||||
state.setIsLoading(true)
|
||||
|
||||
if (!isRerun) {
|
||||
// only clear session state when a new spec is selected
|
||||
Cypress.backend('reset:session:state')
|
||||
// only clear test state when a new spec is selected
|
||||
Cypress.backend('reset:cached:test:state')
|
||||
}
|
||||
|
||||
// when we are re-running we first need to stop cypress always
|
||||
|
||||
@@ -61,7 +61,7 @@ const firstRecordKey = computed(() => {
|
||||
const recordCommand = computed(() => {
|
||||
const componentFlagOrSpace = props.gql.currentTestingType === 'component' ? ' --component ' : ' '
|
||||
|
||||
return `cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
return `npx cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SpecsListBanners from './SpecsListBanners.vue'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { SpecsListBannersFragment, SpecsListBannersFragmentDoc } from '../generated/graphql-test'
|
||||
import { SpecsListBannersFragment, SpecsListBannersFragmentDoc, UseCohorts_DetermineCohortDocument } from '../generated/graphql-test'
|
||||
import interval from 'human-interval'
|
||||
import { CloudUserStubs, CloudProjectStubs } from '@packages/graphql/test/stubCloudTypes'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
@@ -92,6 +92,12 @@ describe('<SpecsListBanners />', () => {
|
||||
})
|
||||
|
||||
context('banner conditions are met and when cypress use >= 4 days', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => {
|
||||
return defineResult({ determineCohort: { __typename: 'Cohort', name: 'foo', cohort: 'A' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('should render when not previously-dismissed', () => {
|
||||
mountWithState(gql, stateWithFirstOpenedDaysAgo(4))
|
||||
cy.get(`[data-cy="${bannerTestId}"]`).should('be.visible')
|
||||
|
||||
@@ -116,19 +116,22 @@
|
||||
:has-banner-been-shown="hasRecordBannerBeenShown"
|
||||
/>
|
||||
<ConnectProjectBanner
|
||||
v-else-if="showConnectBanner"
|
||||
v-else-if="showConnectBanner && cohorts.connectProject?.value"
|
||||
v-model="showConnectBanner"
|
||||
:has-banner-been-shown="hasConnectBannerBeenShown"
|
||||
:cohort-option="cohorts.connectProject.value"
|
||||
/>
|
||||
<CreateOrganizationBanner
|
||||
v-else-if="showCreateOrganizationBanner"
|
||||
v-else-if="showCreateOrganizationBanner && cohorts.organization?.value"
|
||||
v-model="showCreateOrganizationBanner"
|
||||
:has-banner-been-shown="hasCreateOrganizationBannerBeenShown"
|
||||
:cohort-option="cohorts.organization.value"
|
||||
/>
|
||||
<LoginBanner
|
||||
v-else-if="showLoginBanner"
|
||||
v-else-if="showLoginBanner && cohorts.login?.value"
|
||||
v-model="showLoginBanner"
|
||||
:has-banner-been-shown="hasLoginBannerBeenShown"
|
||||
:cohort-option="cohorts.login.value"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -143,13 +146,14 @@ import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import WarningIcon from '~icons/cy/warning_x16.svg'
|
||||
import RefreshIcon from '~icons/cy/action-restart_x16'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import RequestAccessButton from './RequestAccessButton.vue'
|
||||
import { gql, useSubscription } from '@urql/vue'
|
||||
import { SpecsListBannersFragment, SpecsListBanners_CheckCloudOrgMembershipDocument } from '../generated/graphql'
|
||||
import interval from 'human-interval'
|
||||
import { AllowedState, BannerIds } from '@packages/types'
|
||||
import { LoginBanner, CreateOrganizationBanner, ConnectProjectBanner, RecordBanner } from './banners'
|
||||
import { CohortConfig, useCohorts } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
@@ -228,10 +232,10 @@ const showConnectBanner = ref(false)
|
||||
const showCreateOrganizationBanner = ref(false)
|
||||
const showLoginBanner = ref(false)
|
||||
|
||||
const hasRecordBannerBeenShown = ref(true)
|
||||
const hasConnectBannerBeenShown = ref(true)
|
||||
const hasCreateOrganizationBannerBeenShown = ref(true)
|
||||
const hasLoginBannerBeenShown = ref(true)
|
||||
const hasRecordBannerBeenShown = ref(false)
|
||||
const hasConnectBannerBeenShown = ref(false)
|
||||
const hasCreateOrganizationBannerBeenShown = ref(false)
|
||||
const hasLoginBannerBeenShown = ref(false)
|
||||
|
||||
watch(
|
||||
() => ([props.isSpecNotFound, props.isOffline, props.isFetchError, props.isProjectNotFound, props.isProjectUnauthorized]),
|
||||
@@ -272,6 +276,50 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const bannerCohortOptions = {
|
||||
[BannerIds.ACI_082022_LOGIN]: [
|
||||
{ cohort: 'A', value: t('specPage.banners.login.contentA') },
|
||||
{ cohort: 'B', value: t('specPage.banners.login.contentB') },
|
||||
],
|
||||
[BannerIds.ACI_082022_CREATE_ORG]: [
|
||||
{ cohort: 'A', value: t('specPage.banners.createOrganization.titleA') },
|
||||
{ cohort: 'B', value: t('specPage.banners.createOrganization.titleB') },
|
||||
],
|
||||
[BannerIds.ACI_082022_CONNECT_PROJECT]: [
|
||||
{ cohort: 'A', value: t('specPage.banners.connectProject.contentA') },
|
||||
{ cohort: 'B', value: t('specPage.banners.connectProject.contentB') },
|
||||
],
|
||||
}
|
||||
|
||||
const cohortBuilder = useCohorts()
|
||||
|
||||
const getCohortForBanner = (bannerId: string) => {
|
||||
const cohortConfig: CohortConfig = {
|
||||
name: bannerId,
|
||||
options: bannerCohortOptions[bannerId],
|
||||
}
|
||||
|
||||
return cohortBuilder.getCohort(cohortConfig)
|
||||
}
|
||||
|
||||
type BannerType = 'login' | 'connectProject' | 'organization'
|
||||
|
||||
const cohorts: Partial<Record<BannerType, ReturnType<typeof getCohortForBanner>>> = {}
|
||||
|
||||
watchEffect(() => {
|
||||
if (!cohorts.login && showLoginBanner.value) {
|
||||
cohorts.login = getCohortForBanner(BannerIds.ACI_082022_LOGIN)
|
||||
}
|
||||
|
||||
if (!cohorts.organization && showCreateOrganizationBanner.value) {
|
||||
cohorts.organization = getCohortForBanner(BannerIds.ACI_082022_CREATE_ORG)
|
||||
}
|
||||
|
||||
if (!cohorts.connectProject && showConnectBanner.value) {
|
||||
cohorts.connectProject = getCohortForBanner(BannerIds.ACI_082022_CONNECT_PROJECT)
|
||||
}
|
||||
})
|
||||
|
||||
function hasBannerBeenDismissed (bannerId: string) {
|
||||
const bannersState = (props.gql.currentProject?.savedState as AllowedState)?.banners
|
||||
|
||||
|
||||
@@ -3,32 +3,44 @@ import ConnectProjectBanner from './ConnectProjectBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<ConnectProjectBanner />', () => {
|
||||
const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.connectProject.contentA }
|
||||
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.contentA).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.connectProject.buttonLabel).should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={false} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Create project',
|
||||
medium: 'Specs Create Project Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Create project',
|
||||
medium: 'Specs Create Project Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: 'A',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount({ render: () => <ConnectProjectBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_CONNECT_PROJECT"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="connect-project-banner"
|
||||
status="info"
|
||||
@@ -12,12 +12,12 @@
|
||||
:event-data="{
|
||||
campaign: 'Create project',
|
||||
medium: 'Specs Create Project Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: cohortOption.cohort
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.connectProject.content') }}
|
||||
{{ cohortOption.value }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
@@ -45,6 +45,7 @@ import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { ref } from 'vue'
|
||||
import { ConnectProjectBannerDocument } from '../../generated/graphql'
|
||||
@@ -56,19 +57,18 @@ query ConnectProjectBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
cohortOption: CohortOption
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_CONNECT_PROJECT
|
||||
const isProjectConnectOpen = ref(false)
|
||||
|
||||
const cloudModalsQuery = useQuery({ query: ConnectProjectBannerDocument, pause: true })
|
||||
|
||||
@@ -3,6 +3,8 @@ import CreateOrganizationBanner from './CreateOrganizationBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<CreateOrganizationBanner />', () => {
|
||||
const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.createOrganization.titleA }
|
||||
|
||||
it('should render expected content', () => {
|
||||
const linkHref = 'http://dummy.cypress.io/organizations/create'
|
||||
|
||||
@@ -12,9 +14,9 @@ describe('<CreateOrganizationBanner />', () => {
|
||||
cloudOrganizationsUrl: linkHref,
|
||||
} as any
|
||||
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.titleA).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.createOrganization.buttonLabel).should('be.visible')
|
||||
|
||||
@@ -25,22 +27,32 @@ describe('<CreateOrganizationBanner />', () => {
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={false} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Set up your organization',
|
||||
medium: 'Specs Create Organization Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Set up your organization',
|
||||
medium: 'Specs Create Organization Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: 'A',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount({ render: () => <CreateOrganizationBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_CREATE_ORG"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="create-organization-banner"
|
||||
status="info"
|
||||
:title="t('specPage.banners.createOrganization.title')"
|
||||
:title="cohortOption.value"
|
||||
class="mb-16px"
|
||||
:icon="OrganizationIcon"
|
||||
dismissible
|
||||
@@ -12,7 +12,7 @@
|
||||
:event-data="{
|
||||
campaign: 'Set up your organization',
|
||||
medium: 'Specs Create Organization Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: cohortOption.cohort
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
@@ -36,6 +36,7 @@
|
||||
import OrganizationIcon from '~icons/cy/office-building_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import { CreateOrganizationBannerDocument } from '../../generated/graphql'
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
@@ -52,19 +53,18 @@ query CreateOrganizationBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
cohortOption: CohortOption
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_CREATE_ORG
|
||||
|
||||
const query = useQuery({ query: CreateOrganizationBannerDocument })
|
||||
|
||||
@@ -80,6 +80,7 @@ const createOrganizationUrl = computed(() => {
|
||||
params: {
|
||||
utm_medium: 'Specs Create Organization Banner',
|
||||
utm_campaign: 'Set up your organization',
|
||||
utm_content: props.cohortOption.cohort,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,32 +3,44 @@ import LoginBanner from './LoginBanner.vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<LoginBanner />', () => {
|
||||
const cohortOption = { cohort: 'A', value: defaultMessages.specPage.banners.login.contentA }
|
||||
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.contains(defaultMessages.specPage.banners.login.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.login.content).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.login.contentA).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.login.buttonLabel).should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('should record expected event on mount', () => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
context('events', () => {
|
||||
beforeEach(() => {
|
||||
const recordEvent = cy.stub().as('recordEvent')
|
||||
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => {
|
||||
recordEvent(event)
|
||||
|
||||
return defineResult({ recordEvent: true })
|
||||
return defineResult({ recordEvent: true })
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
it('should record expected event on mount', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={false} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Log In',
|
||||
medium: 'Specs Login Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cy.get('@recordEvent').should('have.been.calledWith', {
|
||||
campaign: 'Log In',
|
||||
medium: 'Specs Login Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: cohortOption.cohort,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not record event on mount if already shown', () => {
|
||||
cy.mount({ render: () => <LoginBanner modelValue={true} hasBannerBeenShown={true} cohortOption={cohortOption}/> })
|
||||
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_LOGIN"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="login-banner"
|
||||
status="info"
|
||||
@@ -12,12 +12,12 @@
|
||||
:event-data="{
|
||||
campaign: 'Log In',
|
||||
medium: 'Specs Login Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: cohortOption.cohort
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<p class="mb-24px">
|
||||
{{ t('specPage.banners.login.content') }}
|
||||
{{ cohortOption.value }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
@@ -33,6 +33,7 @@
|
||||
v-model="isLoginOpen"
|
||||
:gql="loginModalQuery.data.value"
|
||||
utm-medium="Specs Login Banner"
|
||||
:utm-content="cohortOption.cohort"
|
||||
/>
|
||||
</TrackedBanner>
|
||||
</template>
|
||||
@@ -45,6 +46,7 @@ import { useI18n } from '@cy/i18n'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { LoginBannerDocument } from '../../generated/graphql'
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import type { CohortOption } from '@packages/frontend-shared/src/composables/useCohorts'
|
||||
import { BannerIds } from '@packages/types'
|
||||
import LoginModal from '@cy/gql-components/topnav/LoginModal.vue'
|
||||
|
||||
@@ -54,19 +56,18 @@ query LoginBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
cohortOption: CohortOption
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_LOGIN
|
||||
const isLoginOpen = ref(false)
|
||||
const loginModalQuery = useQuery({ query: LoginBannerDocument, pause: true })
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TrackedBanner_RecordBannerSeenDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<RecordBanner />', () => {
|
||||
it('should render expected content', () => {
|
||||
cy.mount({ render: () => <RecordBanner modelValue={true} /> })
|
||||
cy.mount({ render: () => <RecordBanner modelValue={true} hasBannerBeenShown={false} /> })
|
||||
|
||||
cy.gqlStub.Query.currentProject = {
|
||||
id: 'test_id',
|
||||
@@ -24,7 +24,7 @@ describe('<RecordBanner />', () => {
|
||||
cy.contains(defaultMessages.specPage.banners.record.title).should('be.visible')
|
||||
cy.contains(defaultMessages.specPage.banners.record.content).should('be.visible')
|
||||
|
||||
cy.findByText('cypress run --component --record --key abcd-efg-1234')
|
||||
cy.findByText('npx cypress run --component --record --key abcd-efg-1234')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
@@ -44,7 +44,7 @@ describe('<RecordBanner />', () => {
|
||||
campaign: 'Record Runs',
|
||||
medium: 'Specs Record Runs Banner',
|
||||
messageId: Cypress.sinon.match.string,
|
||||
cohort: null,
|
||||
cohort: 'n/a',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<TrackedBanner
|
||||
:banner-id="BannerIds.ACI_082022_RECORD"
|
||||
:banner-id="bannerId"
|
||||
:model-value="modelValue"
|
||||
data-cy="record-banner"
|
||||
status="info"
|
||||
@@ -12,7 +12,7 @@
|
||||
:event-data="{
|
||||
campaign: 'Record Runs',
|
||||
medium: 'Specs Record Runs Banner',
|
||||
cohort: '' // TODO Connect cohort
|
||||
cohort: 'n/a'
|
||||
}"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
>
|
||||
@@ -58,29 +58,28 @@ query RecordBanner {
|
||||
}
|
||||
`
|
||||
|
||||
withDefaults(defineProps<{
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
hasBannerBeenShown: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
hasBannerBeenShown: true,
|
||||
})
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const bannerId = BannerIds.ACI_082022_RECORD
|
||||
|
||||
const query = useQuery({ query: RecordBannerDocument })
|
||||
|
||||
const firstRecordKey = computed(() => {
|
||||
return (query.data?.value?.currentProject?.cloudProject?.__typename === 'CloudProject' && query.data.value.currentProject.cloudProject.recordKeys?.[0]?.key) ?? '<record-key>'
|
||||
})
|
||||
|
||||
const recordCommand = computed(() => {
|
||||
const componentFlagOrSpace = query.data?.value?.currentProject?.currentTestingType === 'component' ? ' --component ' : ' '
|
||||
|
||||
return `cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
return `npx cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}`
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import TrackedBanner from './TrackedBanner.vue'
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { TrackedBanner_RecordBannerSeenDocument, TrackedBanner_SetProjectStateDocument } from '../../generated/graphql'
|
||||
|
||||
describe('<TrackedBanner />', () => {
|
||||
@@ -25,7 +25,7 @@ describe('<TrackedBanner />', () => {
|
||||
|
||||
// Initially mount as visible
|
||||
// @ts-ignore
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} hasBannerBeenShown={false} /> })
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} hasBannerBeenShown={false} eventData={{} as any}/> })
|
||||
|
||||
cy.get('[data-cy="banner"]').as('banner')
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('<TrackedBanner />', () => {
|
||||
|
||||
// Initially mount as visible
|
||||
// @ts-ignore
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} dismissible hasBannerBeenShown={false} /> })
|
||||
cy.mount({ render: () => <TrackedBanner data-cy="banner" bannerId="test-banner" v-model={shown.value} dismissible hasBannerBeenShown={false} eventData={{} as any} /> })
|
||||
|
||||
cy.get('[data-cy="banner"]').as('banner')
|
||||
|
||||
@@ -74,32 +74,55 @@ describe('<TrackedBanner />', () => {
|
||||
})
|
||||
|
||||
context('when banner not previously shown', () => {
|
||||
let eventData
|
||||
|
||||
beforeEach(() => {
|
||||
const setProjectStateStub = cy.stub().as('setProjectState')
|
||||
const hasBannerBeenShown = ref(false)
|
||||
|
||||
// mock setting the project state which would reactively set the hasBannerBeenShown ref
|
||||
cy.stubMutationResolver(TrackedBanner_SetProjectStateDocument, (defineResult, event) => {
|
||||
setProjectStateStub(event)
|
||||
const preference = JSON.parse(event.value)
|
||||
|
||||
expect(preference).to.have.nested.property('banners.test-banner.lastShown')
|
||||
hasBannerBeenShown.value = true
|
||||
|
||||
return defineResult({ setPreferences: null }) // do not care about return value here
|
||||
})
|
||||
|
||||
eventData = reactive({ campaign: 'CAM', medium: 'MED', cohort: 'COH' })
|
||||
|
||||
cy.mount({
|
||||
render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={false} eventData={{ campaign: 'CAM', medium: 'MED', cohort: 'COH' }} />,
|
||||
render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={hasBannerBeenShown.value} eventData={eventData} />,
|
||||
})
|
||||
})
|
||||
|
||||
it('should record event', () => {
|
||||
cy.get('@recordEvent').should('have.been.calledOnce')
|
||||
eventData.cohort = 'COH2' //Change reactive variable to confirm the record event is not recorded a second time
|
||||
cy.get('@recordEvent').should(
|
||||
'have.been.calledWith',
|
||||
'have.been.calledOnceWith',
|
||||
Cypress.sinon.match({ campaign: 'CAM', messageId: Cypress.sinon.match.string, medium: 'MED', cohort: 'COH' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should debounce event recording', () => {
|
||||
eventData.cohort = 'COH'
|
||||
cy.wait(250)
|
||||
cy.get('@recordEvent').should('have.been.calledOnce')
|
||||
})
|
||||
})
|
||||
|
||||
context('when banner has been previously shown', () => {
|
||||
let eventData
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mount({ render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={true} eventData={{} as any} /> })
|
||||
eventData = reactive({ campaign: 'CAM', medium: 'MED', cohort: undefined })
|
||||
cy.mount({ render: () => <TrackedBanner bannerId="test-banner" modelValue={true} hasBannerBeenShown={true} eventData={eventData} /> })
|
||||
})
|
||||
|
||||
it('should not record event', () => {
|
||||
eventData.cohort = 'COH'
|
||||
cy.get('@recordEvent').should('not.have.been.called')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
BrowserActions,
|
||||
DevActions,
|
||||
AuthActions,
|
||||
CohortsActions,
|
||||
} from './actions'
|
||||
import { ErrorActions } from './actions/ErrorActions'
|
||||
import { EventCollectorActions } from './actions/EventCollectorActions'
|
||||
@@ -83,4 +84,9 @@ export class DataActions {
|
||||
get eventCollector () {
|
||||
return new EventCollectorActions(this.ctx)
|
||||
}
|
||||
|
||||
@cached
|
||||
get cohorts () {
|
||||
return new CohortsActions(this.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import _ from 'lodash'
|
||||
|
||||
import 'server-destroy'
|
||||
|
||||
import { AppApiShape, DataEmitterActions, LocalSettingsApiShape, ProjectApiShape } from './actions'
|
||||
import { AppApiShape, CohortsApiShape, DataEmitterActions, LocalSettingsApiShape, ProjectApiShape } from './actions'
|
||||
import type { NexusGenAbstractTypeMembers } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { AuthApiShape } from './actions/AuthActions'
|
||||
import type { ElectronApiShape } from './actions/ElectronActions'
|
||||
@@ -70,6 +70,7 @@ export interface DataContextConfig {
|
||||
projectApi: ProjectApiShape
|
||||
electronApi: ElectronApiShape
|
||||
browserApi: BrowserApiShape
|
||||
cohortsApi: CohortsApiShape
|
||||
}
|
||||
|
||||
export interface GraphQLRequestInfo {
|
||||
@@ -131,6 +132,10 @@ export class DataContext {
|
||||
return this._config.localSettingsApi
|
||||
}
|
||||
|
||||
get cohortsApi () {
|
||||
return this._config.cohortsApi
|
||||
}
|
||||
|
||||
get isGlobalMode () {
|
||||
return this.appData.isGlobalMode
|
||||
}
|
||||
@@ -324,6 +329,7 @@ export class DataContext {
|
||||
projectApi: this._config.projectApi,
|
||||
electronApi: this._config.electronApi,
|
||||
localSettingsApi: this._config.localSettingsApi,
|
||||
cohortsApi: this._config.cohortsApi,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
packages/data-context/src/actions/CohortsActions.ts
Normal file
54
packages/data-context/src/actions/CohortsActions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Cohort } from '@packages/types'
|
||||
import type { DataContext } from '..'
|
||||
import { WEIGHTED, WEIGHTED_EVEN } from '../util/weightedChoice'
|
||||
const debug = require('debug')('cypress:data-context:actions:CohortActions')
|
||||
|
||||
export interface CohortsApiShape {
|
||||
getCohorts(): Promise<Record<string, Cohort> | undefined>
|
||||
|
||||
getCohort(name: string): Promise<Cohort | undefined>
|
||||
|
||||
insertCohort (cohort: Cohort): Promise<void>
|
||||
}
|
||||
|
||||
export class CohortsActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
async getCohorts () {
|
||||
debug('Getting all cohorts')
|
||||
|
||||
return this.ctx._apis.cohortsApi.getCohorts()
|
||||
}
|
||||
|
||||
async getCohort (name: string) {
|
||||
debug('Getting cohort for %s', name)
|
||||
|
||||
return this.ctx._apis.cohortsApi.getCohort(name)
|
||||
}
|
||||
|
||||
async determineCohort (name: string, cohorts: string[], weights?: number[]) {
|
||||
debug('Determining cohort', name, cohorts)
|
||||
|
||||
const cohortFromCache = await this.getCohort(name)
|
||||
|
||||
let cohortSelected: Cohort
|
||||
|
||||
if (!cohortFromCache || !cohorts.includes(cohortFromCache.cohort)) {
|
||||
const algorithm = weights ? WEIGHTED(weights) : WEIGHTED_EVEN(cohorts)
|
||||
const pickedCohort = {
|
||||
name,
|
||||
cohort: algorithm.pick(cohorts),
|
||||
}
|
||||
|
||||
debug('Inserting cohort for %o', pickedCohort)
|
||||
await this.ctx._apis.cohortsApi.insertCohort(pickedCohort)
|
||||
cohortSelected = pickedCohort
|
||||
} else {
|
||||
cohortSelected = cohortFromCache
|
||||
}
|
||||
|
||||
debug('Selecting cohort', cohortSelected)
|
||||
|
||||
return cohortSelected
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
export * from './AppActions'
|
||||
export * from './AuthActions'
|
||||
export * from './BrowserActions'
|
||||
export * from './CohortsActions'
|
||||
export * from './DataEmitterActions'
|
||||
export * from './DevActions'
|
||||
export * from './ElectronActions'
|
||||
|
||||
@@ -527,6 +527,7 @@ export class ProjectConfigManager {
|
||||
}
|
||||
|
||||
async getFullInitialConfig (options: Partial<AllModeOptions> = this.options.ctx.modeOptions, withBrowsers = true): Promise<FullConfig> {
|
||||
// return cached configuration for new spec and/or new navigating load when Cypress is running tests
|
||||
if (this._cachedFullConfig) {
|
||||
return this._cachedFullConfig
|
||||
}
|
||||
|
||||
@@ -120,10 +120,7 @@ export class HtmlDataSource {
|
||||
window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)};
|
||||
window.__CYPRESS_TESTING_TYPE__ = '${this.ctx.coreData.currentTestingType}'
|
||||
window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.activeBrowser)}
|
||||
${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET
|
||||
? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';`
|
||||
: ''
|
||||
}
|
||||
${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET ? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';` : ''}
|
||||
</script>
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './file'
|
||||
export * from './hasTypescript'
|
||||
export * from './pluginHandlers'
|
||||
export * from './urqlCacheKeys'
|
||||
export * from './weightedChoice'
|
||||
|
||||
57
packages/data-context/src/util/weightedChoice.ts
Normal file
57
packages/data-context/src/util/weightedChoice.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
export type WeightedAlgorithm = {
|
||||
pick: (values: string[]) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomly choose an index from an array based on weights
|
||||
*
|
||||
* Based on algorithm found here: https://dev.to/trekhleb/weighted-random-algorithm-in-javascript-1pdc
|
||||
*
|
||||
* @param weights array of numbered weights that correspond to the indexed values
|
||||
* @param values array of values to choose from
|
||||
*/
|
||||
const weightedChoice = (weights: number[], values: any[]) => {
|
||||
if (weights.length === 0 || values.length === 0 || weights.length !== values.length) {
|
||||
throw new Error('The length of the weights and values must be the same and greater than zero')
|
||||
}
|
||||
|
||||
const cumulativeWeights = weights.reduce<number[]>((acc, curr) => {
|
||||
if (acc.length === 0) {
|
||||
return [curr]
|
||||
}
|
||||
|
||||
const last = acc[acc.length - 1]
|
||||
|
||||
if (!last) {
|
||||
return acc
|
||||
}
|
||||
|
||||
return [...acc, last + curr]
|
||||
}, [])
|
||||
|
||||
const randomNumber = Math.random() * (cumulativeWeights[cumulativeWeights.length - 1] ?? 1)
|
||||
|
||||
const choice = _.transform(cumulativeWeights, (result, value, index) => {
|
||||
if (value >= randomNumber) {
|
||||
result.chosenIndex = index
|
||||
}
|
||||
|
||||
return result.chosenIndex === -1
|
||||
}, { chosenIndex: -1 })
|
||||
|
||||
return values[choice.chosenIndex]
|
||||
}
|
||||
|
||||
export const WEIGHTED = (weights: number[]): WeightedAlgorithm => {
|
||||
return {
|
||||
pick: (values: any[]): string => {
|
||||
return weightedChoice(weights, values)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const WEIGHTED_EVEN = (values: any[]): WeightedAlgorithm => {
|
||||
return WEIGHTED(_.fill(Array(values.length), 1))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { DataContext } from '../../../src'
|
||||
import { CohortsActions } from '../../../src/actions/CohortsActions'
|
||||
import { createTestDataContext } from '../helper'
|
||||
import { expect } from 'chai'
|
||||
import sinon, { SinonStub, match } from 'sinon'
|
||||
|
||||
describe('CohortsActions', () => {
|
||||
let ctx: DataContext
|
||||
let actions: CohortsActions
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.restore()
|
||||
|
||||
ctx = createTestDataContext('open')
|
||||
|
||||
actions = new CohortsActions(ctx)
|
||||
})
|
||||
|
||||
context('getCohort', () => {
|
||||
it('should return null if name not found', async () => {
|
||||
const name = '123'
|
||||
|
||||
const cohort = await actions.getCohort(name)
|
||||
|
||||
expect(cohort).to.be.undefined
|
||||
expect(ctx.cohortsApi.getCohort).to.have.been.calledWith(name)
|
||||
})
|
||||
|
||||
it('should return cohort if in cache', async () => {
|
||||
const cohort = {
|
||||
name: 'loginBanner',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
;(ctx._apis.cohortsApi.getCohort as SinonStub).resolves(cohort)
|
||||
|
||||
const cohortReturned = await actions.getCohort(cohort.name)
|
||||
|
||||
expect(cohortReturned).to.eq(cohort)
|
||||
expect(ctx.cohortsApi.getCohort).to.have.been.calledWith(cohort.name)
|
||||
})
|
||||
})
|
||||
|
||||
context('determineCohort', () => {
|
||||
it('should determine cohort', async () => {
|
||||
const cohortConfig = {
|
||||
name: 'loginBanner',
|
||||
cohorts: ['A', 'B'],
|
||||
}
|
||||
|
||||
const pickedCohort = await actions.determineCohort(cohortConfig.name, cohortConfig.cohorts)
|
||||
|
||||
expect(ctx.cohortsApi.insertCohort).to.have.been.calledOnceWith({ name: cohortConfig.name, cohort: match.string })
|
||||
expect(cohortConfig.cohorts.includes(pickedCohort.cohort)).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ import { DataContext, DataContextConfig } from '../../src'
|
||||
import { graphqlSchema } from '@packages/graphql/src/schema'
|
||||
import { remoteSchemaWrapped as schemaCloud } from '@packages/graphql/src/stitching/remoteSchemaWrapped'
|
||||
import type { BrowserApiShape } from '../../src/sources/BrowserDataSource'
|
||||
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../src/actions'
|
||||
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape, CohortsApiShape } from '../../src/actions'
|
||||
import sinon from 'sinon'
|
||||
import { execute, parse } from 'graphql'
|
||||
import { getOperationName } from '@urql/core'
|
||||
@@ -63,6 +63,12 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run',
|
||||
focusActiveBrowserWindow: sinon.stub(),
|
||||
getBrowsers: sinon.stub().resolves([]),
|
||||
} as unknown as BrowserApiShape,
|
||||
cohortsApi: {
|
||||
getCohorts: sinon.stub().resolves(),
|
||||
getCohort: sinon.stub().resolves(),
|
||||
insertCohort: sinon.stub(),
|
||||
determineCohort: sinon.stub().resolves(),
|
||||
} as unknown as CohortsApiShape,
|
||||
})
|
||||
|
||||
const origFetch = ctx.util.fetch
|
||||
|
||||
75
packages/data-context/test/unit/util/weightedChoice.spec.ts
Normal file
75
packages/data-context/test/unit/util/weightedChoice.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { WEIGHTED, WEIGHTED_EVEN } from '../../../src/util/weightedChoice'
|
||||
|
||||
describe('weightedChoice', () => {
|
||||
context('WeightedAlgorithm', () => {
|
||||
it('should error if invalid arguments', () => {
|
||||
const weights = [25, 75, 45]
|
||||
const options = ['A', 'B']
|
||||
|
||||
const func = () => {
|
||||
WEIGHTED(weights).pick(options)
|
||||
}
|
||||
|
||||
expect(func).to.throw()
|
||||
})
|
||||
|
||||
it('should error if weights is empty', () => {
|
||||
const weights = []
|
||||
const options = ['A', 'B']
|
||||
|
||||
const func = () => {
|
||||
WEIGHTED(weights).pick(options)
|
||||
}
|
||||
|
||||
expect(func).to.throw()
|
||||
})
|
||||
|
||||
it('should error if options is empty', () => {
|
||||
const weights = [25, 75, 45]
|
||||
const options = []
|
||||
|
||||
const func = () => {
|
||||
WEIGHTED(weights).pick(options)
|
||||
}
|
||||
|
||||
expect(func).to.throw()
|
||||
})
|
||||
|
||||
it('should return an option', () => {
|
||||
const weights = [25, 75]
|
||||
const options = ['A', 'B']
|
||||
const selected = WEIGHTED(weights).pick(options)
|
||||
|
||||
expect(options.includes(selected)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('WEIGHTED_EVEN', () => {
|
||||
it('should return an option', () => {
|
||||
const options = ['A', 'B']
|
||||
const selected = WEIGHTED_EVEN(options).pick(options)
|
||||
|
||||
expect(options.includes(selected)).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('randomness', () => {
|
||||
it('should return values close to supplied weights', () => {
|
||||
const results = {}
|
||||
const options = ['A', 'B']
|
||||
const algorithm = WEIGHTED_EVEN(options)
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const selected = algorithm.pick(options)
|
||||
|
||||
results[selected] ? results[selected]++ : results[selected] = 1
|
||||
}
|
||||
|
||||
Object.keys(results).forEach((key) => {
|
||||
expect(Math.round(results[key] / 100)).to.equal(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -237,6 +237,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
validate: undefined,
|
||||
cookies: null,
|
||||
localStorage: null,
|
||||
sessionStorage: null,
|
||||
hydrated: false,
|
||||
})
|
||||
|
||||
@@ -245,7 +246,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
})
|
||||
|
||||
it('sessions.clearAllSavedSessions()', async () => {
|
||||
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:session').resolves(null)
|
||||
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:sessions', true).resolves(null)
|
||||
|
||||
const sessionsManager = new SessionsManager(CypressInstance, () => {})
|
||||
const sessionsSpy = cy.stub(sessionsManager, 'clearActiveSessions')
|
||||
@@ -253,7 +254,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
await sessionsManager.sessions.clearAllSavedSessions()
|
||||
|
||||
expect(sessionsSpy).to.be.calledOnce
|
||||
expect(cypressSpy).to.be.calledOnceWith('clear:session', null)
|
||||
expect(cypressSpy).to.be.calledOnceWith('clear:sessions', true)
|
||||
})
|
||||
|
||||
describe('.clearCurrentSessionData()', () => {
|
||||
|
||||
@@ -190,7 +190,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: 'session-1',
|
||||
isGlobalSession: false,
|
||||
status: 'created',
|
||||
},
|
||||
})
|
||||
@@ -229,7 +231,8 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
|
||||
expect(sessionInfo).to.deep.eq({
|
||||
id: 'session-1',
|
||||
data: {},
|
||||
isGlobalSession: false,
|
||||
status: 'created',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -245,11 +248,13 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
})
|
||||
|
||||
describe('create session with validation flow', () => {
|
||||
let sessionId
|
||||
|
||||
before(() => {
|
||||
setupTestContext()
|
||||
cy.log('Creating new session with validation to test against')
|
||||
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup, { validate })
|
||||
sessionId = `session-${Cypress.state('test').id}`
|
||||
cy.session(sessionId, setup, { validate })
|
||||
})
|
||||
|
||||
// test must be first to run before blank page visit between each test
|
||||
@@ -267,7 +272,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: sessionId,
|
||||
isGlobalSession: false,
|
||||
status: 'created',
|
||||
},
|
||||
})
|
||||
@@ -327,7 +334,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: `session-${Cypress.state('test').id}`,
|
||||
isGlobalSession: false,
|
||||
status: 'failed',
|
||||
},
|
||||
})
|
||||
@@ -382,17 +391,20 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
})
|
||||
|
||||
describe('restores saved session flow', () => {
|
||||
let sessionId
|
||||
|
||||
before(() => {
|
||||
setupTestContext()
|
||||
cy.log('Creating new session for test')
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup)
|
||||
sessionId = `session-${Cypress.state('test').id}`
|
||||
cy.session(sessionId, setup)
|
||||
.then(() => {
|
||||
// reset and only test restored session
|
||||
resetMocks()
|
||||
})
|
||||
|
||||
cy.log('restore session to test against')
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup)
|
||||
cy.session(sessionId, setup)
|
||||
})
|
||||
|
||||
// test must be first to run before blank page visit between each test
|
||||
@@ -415,7 +427,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: sessionId,
|
||||
isGlobalSession: false,
|
||||
status: 'restored',
|
||||
},
|
||||
})
|
||||
@@ -440,17 +454,20 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
})
|
||||
|
||||
describe('restores saved session with validation flow', () => {
|
||||
let sessionId
|
||||
|
||||
before(() => {
|
||||
setupTestContext()
|
||||
cy.log('Creating new session for test')
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup, { validate })
|
||||
sessionId = `session-${Cypress.state('test').id}`
|
||||
cy.session(sessionId, setup, { validate })
|
||||
.then(() => {
|
||||
// reset and only test restored session
|
||||
resetMocks()
|
||||
})
|
||||
|
||||
cy.log('restore session to test against')
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup, { validate })
|
||||
cy.session(sessionId, setup, { validate })
|
||||
})
|
||||
|
||||
// test must be first to run before blank page visit between each test
|
||||
@@ -473,7 +490,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: sessionId,
|
||||
isGlobalSession: false,
|
||||
status: 'restored',
|
||||
},
|
||||
})
|
||||
@@ -510,11 +529,13 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
})
|
||||
|
||||
describe('recreates existing session flow', () => {
|
||||
let sessionId
|
||||
|
||||
before(() => {
|
||||
setupTestContext()
|
||||
cy.log('Creating new session for test')
|
||||
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup, { validate })
|
||||
sessionId = `session-${Cypress.state('test').id}`
|
||||
cy.session(sessionId, setup, { validate })
|
||||
.then(() => {
|
||||
// reset and only test restored session
|
||||
resetMocks()
|
||||
@@ -528,7 +549,7 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
})
|
||||
|
||||
cy.log('restore session to test against')
|
||||
cy.session(`session-${Cypress.state('test').id}`, setup, { validate })
|
||||
cy.session(sessionId, setup, { validate })
|
||||
})
|
||||
|
||||
// test must be first to run before blank page visit between each test
|
||||
@@ -551,7 +572,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: sessionId,
|
||||
isGlobalSession: false,
|
||||
status: 'recreated',
|
||||
},
|
||||
})
|
||||
@@ -659,7 +682,9 @@ describe('cy.session', { retries: 0 }, () => {
|
||||
expect(logs[0].get()).to.deep.contain({
|
||||
name: 'session',
|
||||
id: sessionGroupId,
|
||||
renderProps: {
|
||||
sessionInfo: {
|
||||
id: `session-${Cypress.state('test').id}`,
|
||||
isGlobalSession: false,
|
||||
status: 'failed',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,110 +1,9 @@
|
||||
const {
|
||||
getSessionDetails,
|
||||
getConsoleProps,
|
||||
navigateAboutBlank,
|
||||
} = require('@packages/driver/src/cy/commands/sessions/utils')
|
||||
|
||||
describe('src/cy/commands/sessions/utils.ts', () => {
|
||||
describe('.getSessionDetails', () => {
|
||||
it('for one domain with neither cookies or local storage set', () => {
|
||||
const sessionState = {
|
||||
id: 'session1',
|
||||
}
|
||||
|
||||
const details = getSessionDetails(sessionState)
|
||||
|
||||
expect(details.id).to.eq('session1')
|
||||
expect(Object.keys(details.data)).to.have.length(0)
|
||||
})
|
||||
|
||||
it('for one domain with only cookies set', () => {
|
||||
const sessionState = {
|
||||
id: 'session1',
|
||||
cookies: [
|
||||
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
|
||||
],
|
||||
}
|
||||
|
||||
const details = getSessionDetails(sessionState)
|
||||
|
||||
expect(details.id).to.eq('session1')
|
||||
expect(Object.keys(details.data)).to.have.length(1)
|
||||
expect(details.data).to.have.property('localhost')
|
||||
expect(details.data.localhost).to.deep.eq({
|
||||
cookies: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('for one domain with only local storage set', () => {
|
||||
const sessionState = {
|
||||
id: 'session1',
|
||||
localStorage: [
|
||||
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
|
||||
],
|
||||
}
|
||||
|
||||
const details = getSessionDetails(sessionState)
|
||||
|
||||
expect(details.id).to.eq('session1')
|
||||
expect(Object.keys(details.data)).to.have.length(1)
|
||||
expect(details.data).to.have.property('localhost')
|
||||
expect(details.data.localhost).to.deep.eq({
|
||||
localStorage: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('for one domain with both cookies and localStorage', () => {
|
||||
const sessionState = {
|
||||
id: 'session1',
|
||||
cookies: [
|
||||
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
|
||||
],
|
||||
localStorage: [
|
||||
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
|
||||
],
|
||||
}
|
||||
|
||||
const details = getSessionDetails(sessionState)
|
||||
|
||||
expect(details.id).to.eq('session1')
|
||||
expect(Object.keys(details.data)).to.have.length(1)
|
||||
expect(details.data).to.have.property('localhost')
|
||||
expect(details.data.localhost).to.deep.eq({
|
||||
cookies: 1,
|
||||
localStorage: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('for multiple domains', () => {
|
||||
const sessionState = {
|
||||
id: 'session1',
|
||||
cookies: [
|
||||
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
|
||||
{ name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expiry: 456 },
|
||||
],
|
||||
localStorage: [
|
||||
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
|
||||
{ origin: 'http://example.com', value: { 'random': 'hi' } },
|
||||
],
|
||||
}
|
||||
|
||||
const details = getSessionDetails(sessionState)
|
||||
|
||||
expect(details.id).to.eq('session1')
|
||||
expect(Object.keys(details.data)).to.have.length(2)
|
||||
expect(details.data).to.have.property('localhost')
|
||||
expect(details.data.localhost).to.deep.eq({
|
||||
cookies: 2,
|
||||
localStorage: 1,
|
||||
})
|
||||
|
||||
expect(details.data).to.have.property('example.com')
|
||||
expect(details.data['example.com']).to.deep.eq({
|
||||
localStorage: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getConsoleProps', () => {
|
||||
it('for one domain with neither cookies or localStorage set', () => {
|
||||
const sessionState = {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { bothUrlsMatchAndOneHasHash } from '../navigation'
|
||||
import { $Location, LocationObject } from '../../cypress/location'
|
||||
import { isRunnerAbleToCommunicateWithAut } from '../../util/commandAUTCommunication'
|
||||
|
||||
import type { RunState } from '@packages/types'
|
||||
|
||||
import debugFn from 'debug'
|
||||
const debug = debugFn('cypress:driver:navigation')
|
||||
|
||||
@@ -1116,26 +1118,26 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
// tell our backend we're changing origins
|
||||
// TODO: add in other things we want to preserve
|
||||
// state for like scrollTop
|
||||
let s: Record<string, any> = {
|
||||
let runState: RunState = {
|
||||
currentId: id,
|
||||
tests: Cypress.runner.getTestsState(),
|
||||
startTime: Cypress.runner.getStartTime(),
|
||||
emissions: Cypress.runner.getEmissions(),
|
||||
}
|
||||
|
||||
s.passed = Cypress.runner.countByTestState(s.tests, 'passed')
|
||||
s.failed = Cypress.runner.countByTestState(s.tests, 'failed')
|
||||
s.pending = Cypress.runner.countByTestState(s.tests, 'pending')
|
||||
s.numLogs = LogUtils.countLogsByTests(s.tests)
|
||||
runState.passed = Cypress.runner.countByTestState(runState.tests, 'passed')
|
||||
runState.failed = Cypress.runner.countByTestState(runState.tests, 'failed')
|
||||
runState.pending = Cypress.runner.countByTestState(runState.tests, 'pending')
|
||||
runState.numLogs = LogUtils.countLogsByTests(runState.tests)
|
||||
|
||||
return Cypress.action('cy:collect:run:state')
|
||||
.then((a = []) => {
|
||||
.then((otherRunStates = []) => {
|
||||
// merge all the states together holla'
|
||||
s = _.reduce(a, (memo, obj) => {
|
||||
runState = _.reduce(otherRunStates, (memo, obj) => {
|
||||
return _.extend(memo, obj)
|
||||
}, s)
|
||||
}, runState)
|
||||
|
||||
return Cypress.backend('preserve:run:state', s)
|
||||
return Cypress.backend('preserve:run:state', runState)
|
||||
})
|
||||
.then(() => {
|
||||
// and now we must change the url to be the new
|
||||
|
||||
@@ -5,7 +5,6 @@ import $stackUtils from '../../../cypress/stack_utils'
|
||||
import logGroup from '../../logGroup'
|
||||
import SessionsManager from './manager'
|
||||
import {
|
||||
getSessionDetails,
|
||||
getConsoleProps,
|
||||
navigateAboutBlank,
|
||||
} from './utils'
|
||||
@@ -118,6 +117,16 @@ export default function (Commands, Cypress, cy) {
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionLogStatus (status: string) {
|
||||
_log.set({
|
||||
sessionInfo: {
|
||||
id: existingSession.id,
|
||||
isGlobalSession: false,
|
||||
status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function createSession (existingSession, recreateSession = false) {
|
||||
logGroup(Cypress, {
|
||||
name: 'session',
|
||||
@@ -178,7 +187,8 @@ export default function (Commands, Cypress, cy) {
|
||||
|
||||
const onFail = (err) => {
|
||||
validateLog.set({ state: 'failed' })
|
||||
_log.set({ renderProps: { status: 'failed' } })
|
||||
setSessionLogStatus('failed')
|
||||
|
||||
// show validation error and allow sessions workflow to recreate the session
|
||||
if (restoreSession) {
|
||||
Cypress.log({
|
||||
@@ -296,7 +306,8 @@ export default function (Commands, Cypress, cy) {
|
||||
*/
|
||||
const createSessionWorkflow = (existingSession, recreateSession = false) => {
|
||||
return cy.then(async () => {
|
||||
_log.set({ renderProps: { status: recreateSession ? 'recreating' : 'creating' } })
|
||||
setSessionLogStatus(recreateSession ? 'recreating' : 'creating')
|
||||
|
||||
await navigateAboutBlank()
|
||||
await sessions.clearCurrentSessionData()
|
||||
|
||||
@@ -308,7 +319,7 @@ export default function (Commands, Cypress, cy) {
|
||||
return
|
||||
}
|
||||
|
||||
_log.set({ renderProps: { status: recreateSession ? 'recreated' : 'created' } })
|
||||
setSessionLogStatus(recreateSession ? 'recreated' : 'created')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -320,7 +331,7 @@ export default function (Commands, Cypress, cy) {
|
||||
*/
|
||||
const restoreSessionWorkflow = (existingSession) => {
|
||||
return cy.then(async () => {
|
||||
_log.set({ renderProps: { status: 'restoring' } })
|
||||
setSessionLogStatus('restoring')
|
||||
await navigateAboutBlank()
|
||||
await sessions.clearCurrentSessionData()
|
||||
|
||||
@@ -332,7 +343,7 @@ export default function (Commands, Cypress, cy) {
|
||||
return createSessionWorkflow(existingSession, true)
|
||||
}
|
||||
|
||||
_log.set({ renderProps: { status: 'restored' } })
|
||||
setSessionLogStatus('restored')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -349,7 +360,6 @@ export default function (Commands, Cypress, cy) {
|
||||
let _log
|
||||
const groupDetails = {
|
||||
message: `${existingSession.id.length > 50 ? `${existingSession.id.substring(0, 47)}...` : existingSession.id}`,
|
||||
sessionInfo: getSessionDetails(existingSession),
|
||||
}
|
||||
|
||||
return logGroup(Cypress, groupDetails, (log) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash'
|
||||
import { $Location } from '../../../cypress/location'
|
||||
|
||||
import type { ServerSessionData } from '@packages/types'
|
||||
import {
|
||||
getCurrentOriginStorage,
|
||||
setPostMessageLocalStorage,
|
||||
@@ -120,6 +120,7 @@ export default class SessionsManager {
|
||||
id: options.id,
|
||||
cookies: null,
|
||||
localStorage: null,
|
||||
sessionStorage: null,
|
||||
setup: options.setup,
|
||||
hydrated: false,
|
||||
validate: options.validate,
|
||||
@@ -132,8 +133,9 @@ export default class SessionsManager {
|
||||
|
||||
clearAllSavedSessions: async () => {
|
||||
this.clearActiveSessions()
|
||||
const clearAllSessions = true
|
||||
|
||||
return this.Cypress.backend('clear:session', null)
|
||||
return this.Cypress.backend('clear:sessions', clearAllSessions)
|
||||
},
|
||||
|
||||
clearCurrentSessionData: async () => {
|
||||
@@ -205,7 +207,7 @@ export default class SessionsManager {
|
||||
}
|
||||
},
|
||||
|
||||
getSession: (id: string) => {
|
||||
getSession: (id: string): Promise<ServerSessionData> => {
|
||||
return this.Cypress.backend('get:session', id)
|
||||
},
|
||||
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import _ from 'lodash'
|
||||
import $ from 'jquery'
|
||||
import { $Location } from '../../../cypress/location'
|
||||
import Bluebird from 'bluebird'
|
||||
import { $Location } from '../../../cypress/location'
|
||||
|
||||
type SessionData = Cypress.Commands.Session.SessionData
|
||||
|
||||
const getSessionDetails = (sessState: SessionData) => {
|
||||
return {
|
||||
id: sessState.id,
|
||||
data: _.merge(
|
||||
_.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v.length })),
|
||||
..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: Object.keys(v.value).length } })),
|
||||
) }
|
||||
}
|
||||
|
||||
const getSessionDetailsForTable = (sessState: SessionData) => {
|
||||
return _.merge(
|
||||
_.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v })),
|
||||
@@ -199,7 +190,6 @@ function navigateAboutBlank (session: boolean = true) {
|
||||
}
|
||||
|
||||
export {
|
||||
getSessionDetails,
|
||||
getCurrentOriginStorage,
|
||||
setPostMessageLocalStorage,
|
||||
getConsoleProps,
|
||||
|
||||
@@ -42,6 +42,8 @@ import * as resolvers from './cypress/resolvers'
|
||||
import { PrimaryOriginCommunicator, SpecBridgeCommunicator } from './cross-origin/communicator'
|
||||
import { setupAutEventHandlers } from './cypress/aut_event_handlers'
|
||||
|
||||
import type { CachedTestState } from '@packages/types'
|
||||
|
||||
const debug = debugFn('cypress:driver:cypress')
|
||||
|
||||
declare global {
|
||||
@@ -280,11 +282,13 @@ class $Cypress {
|
||||
}
|
||||
}
|
||||
|
||||
run (fn) {
|
||||
run (cachedTestState: CachedTestState, fn) {
|
||||
if (!this.runner) {
|
||||
$errUtils.throwErrByPath('miscellaneous.no_runner')
|
||||
}
|
||||
|
||||
this.state(cachedTestState)
|
||||
|
||||
return this.runner.run(fn)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { StateFunc } from './state'
|
||||
const groupsOrTableRe = /^(groups|table)$/
|
||||
const parentOrChildRe = /parent|child|system/
|
||||
const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ')
|
||||
const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ')
|
||||
const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ')
|
||||
const BLACKLIST_PROPS = 'snapshots'.split(' ')
|
||||
|
||||
let counter = 0
|
||||
@@ -642,10 +642,6 @@ class LogManager {
|
||||
|
||||
this.addToLogs(log)
|
||||
|
||||
if (options.sessionInfo) {
|
||||
Cypress.emit('session:add', log.toJSON())
|
||||
}
|
||||
|
||||
if (options.emitOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ declare namespace Cypress {
|
||||
type SessionSetup = (log: Cypress.Log) => Chainable<S>
|
||||
type SessionValidation = (log: Cypress.Log) => Chainable<S>
|
||||
|
||||
interface LocalStorage {
|
||||
interface Storage {
|
||||
origin: string
|
||||
value: Record<string, any>
|
||||
}
|
||||
@@ -14,7 +14,8 @@ declare namespace Cypress {
|
||||
interface SessionData {
|
||||
id: string
|
||||
cookies?: Array<Cypress.Cookie> | null
|
||||
localStorage?: Array<LocalStorage> | null
|
||||
localStorage?: Array<Storage> | null
|
||||
sessionStorage?: Array<Storage> | null
|
||||
setup: () => void
|
||||
hydrated: boolean
|
||||
validate?: Cypress.SessionOptions['validate']
|
||||
|
||||
8
packages/driver/types/cy/logGroup.d.ts
vendored
8
packages/driver/types/cy/logGroup.d.ts
vendored
@@ -17,14 +17,6 @@ declare namespace Cypress {
|
||||
displayName?: string
|
||||
// additional information to include in the log
|
||||
message?: string
|
||||
// session information to associate with the log and be added to the session instrument panel
|
||||
sessionInfo?: {
|
||||
id: string
|
||||
data: {
|
||||
cookies?: Array<Cypress.Cookie> | null
|
||||
localStorage?: Array<LocalStorage> | null
|
||||
}
|
||||
}
|
||||
// timeout of the group command - defaults to defaultCommandTimeout
|
||||
timeout?: number
|
||||
// the type of log
|
||||
|
||||
3
packages/frontend-shared/src/assets/icons/globe_x12.svg
Normal file
3
packages/frontend-shared/src/assets/icons/globe_x12.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.85634 2.4375L9.48916 2.77687L9.85634 2.4375ZM2.14366 2.4375L2.51085 2.77687L2.14366 2.4375ZM2.14366 9.5625L2.51085 9.22313L2.14366 9.5625ZM9.85634 9.5625L9.48916 9.22313L9.85634 9.5625ZM7.37726 2.43747L6.90246 2.5942V2.5942L7.37726 2.43747ZM4.62273 2.43747L5.09753 2.5942L4.62273 2.43747ZM4.62273 9.56247L4.14793 9.7192L4.62273 9.56247ZM7.37726 9.56247L7.85206 9.7192L7.37726 9.56247ZM11.25 5.97854L11.75 5.97654L11.25 5.97854ZM0.750043 5.97855L0.250047 5.97654L0.750043 5.97855ZM6 1.25C7.37931 1.25 8.62071 1.83726 9.48916 2.77687L10.2235 2.09813C9.17374 0.962306 7.66968 0.25 6 0.25V1.25ZM2.51085 2.77687C3.37929 1.83726 4.62069 1.25 6 1.25V0.25C4.33032 0.25 2.82626 0.962306 1.77647 2.09813L2.51085 2.77687ZM0.25 6C0.25 7.50569 0.82939 8.87718 1.77648 9.90188L2.51085 9.22313C1.7278 8.37591 1.25 7.24432 1.25 6H0.25ZM6 10.75C4.62069 10.75 3.37929 10.1627 2.51085 9.22313L1.77648 9.90188C2.82626 11.0377 4.33032 11.75 6 11.75V10.75ZM6 11.75C7.66968 11.75 9.17374 11.0377 10.2235 9.90188L9.48916 9.22313C8.62071 10.1627 7.37931 10.75 6 10.75V11.75ZM10.75 6C10.75 7.24432 10.2722 8.37591 9.48916 9.22313L10.2235 9.90188C11.1706 8.87718 11.75 7.50569 11.75 6H10.75ZM8.375 5.99997C8.375 4.58437 8.18126 3.27803 7.85206 2.28074L6.90246 2.5942C7.19117 3.46883 7.375 4.66556 7.375 5.99997H8.375ZM6 1.25C6.05735 1.25 6.18737 1.28347 6.37373 1.51671C6.55646 1.74539 6.74142 2.10635 6.90246 2.5942L7.85206 2.28074C7.67055 1.73087 7.43912 1.2481 7.15497 0.892474C6.87445 0.541404 6.48711 0.25 6 0.25V1.25ZM4.14793 2.28074C3.81873 3.27803 3.62499 4.58437 3.62499 5.99997H4.62499C4.62499 4.66556 4.80882 3.46883 5.09753 2.5942L4.14793 2.28074ZM5.09753 2.5942C5.25857 2.10635 5.44354 1.74539 5.62626 1.51671C5.81263 1.28347 5.94265 1.25 6 1.25V0.25C5.51289 0.25 5.12555 0.541402 4.84503 0.892471C4.56087 1.24809 4.32944 1.73087 4.14793 2.28074L5.09753 2.5942ZM6 10.75C5.94267 10.75 5.81265 10.7165 5.62627 10.4833C5.44354 10.2546 5.25857 9.8936 5.09753 9.40574L4.14793 9.7192C4.32944 10.2691 4.56087 10.7519 4.84502 11.1075C5.12553 11.4586 5.51288 11.75 6 11.75V10.75ZM6 11.75C6.48713 11.75 6.87447 11.4586 7.15498 11.1075C7.43913 10.7519 7.67056 10.2691 7.85206 9.7192L6.90246 9.40575C6.74142 9.8936 6.55645 10.2546 6.37372 10.4833C6.18735 10.7165 6.05734 10.75 6 10.75V11.75ZM11.75 6C11.75 5.99218 11.75 5.98436 11.75 5.97654L10.75 5.98055C10.75 5.98703 10.75 5.99351 10.75 6H11.75ZM11.75 5.97654C11.744 4.48004 11.1657 3.11748 10.2235 2.09813L9.48916 2.77687C10.2681 3.61969 10.745 4.74393 10.75 5.98055L11.75 5.97654ZM1.77647 2.09813C0.834327 3.11748 0.256034 4.48004 0.250047 5.97654L1.25004 5.98055C1.25499 4.74393 1.73186 3.61969 2.51085 2.77687L1.77647 2.09813ZM0.250047 5.97654C0.250016 5.98436 0.25 5.99218 0.25 6H1.25C1.25 5.99351 1.25001 5.98703 1.25004 5.98055L0.250047 5.97654ZM3.62499 5.99997C3.62499 6.23231 3.63021 6.46138 3.64035 6.68634L4.63934 6.64132C4.62988 6.43143 4.62499 6.21737 4.62499 5.99997H3.62499ZM4.09291 7.16162C4.70657 7.21947 5.34489 7.24997 6 7.24997V6.24997C5.37577 6.24997 4.76877 6.2209 4.18677 6.16604L4.09291 7.16162ZM6 7.24997C6.6551 7.24997 7.29343 7.21947 7.90708 7.16162L7.81322 6.16604C7.23122 6.2209 6.62422 6.24997 6 6.24997V7.24997ZM8.35964 6.68634C8.36978 6.46138 8.375 6.23231 8.375 5.99997H7.375C7.375 6.21737 7.37011 6.43143 7.36066 6.64132L8.35964 6.68634ZM7.90708 7.16162C9.20524 7.03924 10.3987 6.7939 11.41 6.45224L11.0899 5.50485C10.1634 5.81789 9.04736 6.04969 7.81322 6.16604L7.90708 7.16162ZM7.36066 6.64132C7.31212 7.71841 7.14355 8.67536 6.90246 9.40575L7.85206 9.7192C8.12782 8.8838 8.30795 7.83337 8.35964 6.68634L7.36066 6.64132ZM4.18677 6.16604C2.95264 6.04969 1.83665 5.81789 0.91008 5.50485L0.590006 6.45224C1.60127 6.79389 2.79476 7.03924 4.09291 7.16162L4.18677 6.16604ZM3.64035 6.68634C3.69204 7.83337 3.87217 8.8838 4.14793 9.7192L5.09753 9.40574C4.85644 8.67536 4.68787 7.71841 4.63934 6.64132L3.64035 6.68634ZM0.250044 5.97755L0.250001 5.999L1.25 6.001L1.25004 5.97955L0.250044 5.97755ZM11.75 5.999L11.75 5.97754L10.75 5.97954L10.75 6.001L11.75 5.999Z" fill="#9AA2FC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,31 @@
|
||||
import UseCohortsExample, { CopyOption } from './UseCohortsExample.vue'
|
||||
import { UseCohorts_DetermineCohortDocument } from '../../generated/graphql'
|
||||
|
||||
describe('useCohorts example', () => {
|
||||
const copyOptions: CopyOption[] = [
|
||||
{ cohort: 'A', value: 'Notification Title A' },
|
||||
{ cohort: 'B', value: 'Notification Title B' },
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => {
|
||||
return defineResult({ determineCohort: { __typename: 'Cohort', name: 'foo', cohort: 'A' } })
|
||||
})
|
||||
})
|
||||
|
||||
it('should show value for one cohort with default algorithm', () => {
|
||||
cy.mount(() => <UseCohortsExample copyOptions={copyOptions}/>)
|
||||
cy.findByTestId('result').then((elem) => {
|
||||
expect(copyOptions.map((option) => option.value)).to.include(elem.text())
|
||||
})
|
||||
})
|
||||
|
||||
it('should show value for one cohort with supplied algorithm', () => {
|
||||
const weighted25_75 = [25, 75]
|
||||
|
||||
cy.mount(() => <UseCohortsExample copyOptions={copyOptions} weights={weighted25_75}/>)
|
||||
cy.findByTestId('result').then((elem) => {
|
||||
expect(copyOptions.map((option) => option.value)).to.include(elem.text())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div data-cy="result">
|
||||
{{ cohortChoice?.value }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type CopyOption = {
|
||||
cohort: string
|
||||
value: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CohortConfig, useCohorts } from '../useCohorts'
|
||||
|
||||
const props = defineProps<{
|
||||
weights?: number[]
|
||||
copyOptions: CopyOption[]
|
||||
}>()
|
||||
|
||||
const cohortConfig: CohortConfig = {
|
||||
name: 'login',
|
||||
options: props.copyOptions,
|
||||
weights: props.weights,
|
||||
}
|
||||
|
||||
const cohortChoice = useCohorts().getCohort(cohortConfig)
|
||||
|
||||
</script>
|
||||
83
packages/frontend-shared/src/composables/useCohorts.ts
Normal file
83
packages/frontend-shared/src/composables/useCohorts.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useMutation, gql } from '@urql/vue'
|
||||
import { UseCohorts_DetermineCohortDocument } from '../generated/graphql'
|
||||
import { ref } from 'vue'
|
||||
|
||||
gql`
|
||||
mutation UseCohorts_DetermineCohort ($name: String!, $cohorts: [String!]!) {
|
||||
determineCohort(cohortConfig: { name: $name, cohorts: $cohorts } ) {
|
||||
__typename
|
||||
name
|
||||
cohort
|
||||
}
|
||||
}`
|
||||
|
||||
/**
|
||||
* An option to use for a given cohort selection.
|
||||
*/
|
||||
export type CohortOption = {
|
||||
/** The individual cohort identifier. Example: 'A' or 'B' */
|
||||
cohort: string
|
||||
|
||||
/** The value to be used by the calling code for the given cohort. The algorithm for selecting the cohort does not care about this value, but it will return the entire CohortOption that is selected so that this value can be used by the calling code. */
|
||||
value: any
|
||||
}
|
||||
|
||||
/**
|
||||
* The configuration of the cohort to be used to select a cohort.
|
||||
*/
|
||||
export type CohortConfig = {
|
||||
/** The name of the feature the cohort will be calculated for. This will be used as a key in the cache file for storing the selected option. */
|
||||
name: string
|
||||
|
||||
/** Array of options to pick from when selecting the cohort */
|
||||
options: CohortOption[]
|
||||
|
||||
/** Optional array of weights to use for selecting the cohort. If not supplied, an even weighting algorithm will be used. */
|
||||
weights?: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that encapsulates the logic for choosing a cohort from a list of configured options.
|
||||
*
|
||||
* @remarks
|
||||
* The logic for this composable will first check the cache file to determine if a cohort has already been saved for the given cohort `name`. If found, that cohort will be returned. If not found or the option found does not match an existing option, a weighted algorithm will be used to pick from the list of CohortOptions. The picked value will be stored in the cache and returned.
|
||||
*
|
||||
* @returns object with getCohort function for returning the cohort
|
||||
*/
|
||||
export const useCohorts = () => {
|
||||
const determineCohortMutation = useMutation(UseCohorts_DetermineCohortDocument)
|
||||
|
||||
const determineCohort = async (name: string, cohorts: string[]) => {
|
||||
return await determineCohortMutation.executeMutation({
|
||||
name,
|
||||
cohorts,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cohort from the list of configured options
|
||||
*
|
||||
* @param config - cohort configuration that contains the options to choose from and optionally the algorithm to use. Defaults to using the WEIGHTED_EVEN algorithm
|
||||
*
|
||||
* @returns a reactive reference to the cohort option that is selected
|
||||
*/
|
||||
const getCohort = (config: CohortConfig) => {
|
||||
const cohortOptionSelected = ref<CohortOption>()
|
||||
|
||||
const cohortIds = config.options.map((option) => option.cohort)
|
||||
|
||||
const fetchCohort = async () => {
|
||||
const cohortSelected = await determineCohort(config.name, cohortIds)
|
||||
|
||||
cohortOptionSelected.value = config.options.find((option) => option.cohort === cohortSelected.data?.determineCohort?.cohort)
|
||||
}
|
||||
|
||||
fetchCohort()
|
||||
|
||||
return cohortOptionSelected
|
||||
}
|
||||
|
||||
return {
|
||||
getCohort,
|
||||
}
|
||||
}
|
||||
@@ -197,17 +197,20 @@
|
||||
"banners": {
|
||||
"login": {
|
||||
"title": "Optimize and record your CI test runs with Cypress Dashboard",
|
||||
"content": "Parallelize your tests in CI and visualize every error by watching full video recordings of each test you run.",
|
||||
"contentA": "Parallelize your tests in CI and visualize every error by watching full video recordings of each test you run.",
|
||||
"contentB": "When you configure Cypress to record tests to the Cypress Dashboard, you'll see data from your latest recorded runs in the Cypress app. This increased visibility into your test history allows you to debug your tests faster and more effectively, all within your local workflow.",
|
||||
"buttonLabel": "Get started with Cypress Dashboard"
|
||||
},
|
||||
"createOrganization": {
|
||||
"title": "Finish setting up Cypress Dashboard",
|
||||
"titleA": "Finish setting up Cypress Dashboard",
|
||||
"titleB": "Create or join an organization",
|
||||
"content": "Join or create an organization in Cypress Dashboard to access your projects and recorded test runs.",
|
||||
"buttonLabel": "Set up your organization"
|
||||
},
|
||||
"connectProject": {
|
||||
"title": "Connect your project to Cypress Dashboard",
|
||||
"content": "View recorded test runs directly in the Cypress app to monitor, run, and fix tests locally.",
|
||||
"contentA": "View recorded test runs directly in the Cypress app to monitor, run, and fix tests locally.",
|
||||
"contentB": "Bring your recorded test results into your local development workflow to monitor, run, and fix tests all in the Cypress app.",
|
||||
"buttonLabel": "Connect your project"
|
||||
},
|
||||
"record": {
|
||||
|
||||
@@ -555,6 +555,28 @@ enum CodeLanguageEnum {
|
||||
ts
|
||||
}
|
||||
|
||||
"""used to distinguish one group of users from another"""
|
||||
type Cohort {
|
||||
"""value used to indicate the cohort (e.g. "A" or "B")"""
|
||||
cohort: String!
|
||||
|
||||
"""name used to identify the cohort topic (e.g. "LoginBanner" ) """
|
||||
name: String!
|
||||
}
|
||||
|
||||
input CohortInput {
|
||||
"""Array of cohort options to choose from. Ex: A or B """
|
||||
cohorts: [String!]!
|
||||
|
||||
"""Name of the cohort"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
Optional array of integer weights to use for determining cohort. Defaults to even weighting
|
||||
"""
|
||||
weights: [Int!]
|
||||
}
|
||||
|
||||
"""
|
||||
The currently opened Cypress project, represented by a cypress.config.{js,ts,mjs,cjs} file
|
||||
"""
|
||||
@@ -1198,6 +1220,11 @@ type Mutation {
|
||||
"""add the passed text to the local clipboard"""
|
||||
copyTextToClipboard(text: String!): Boolean
|
||||
|
||||
"""
|
||||
Determine the cohort based on the given configuration. This will either return the cached cohort for a given name or choose a new one and store it.
|
||||
"""
|
||||
determineCohort(cohortConfig: CohortInput!): Cohort
|
||||
|
||||
"""
|
||||
Development only: Triggers or dismisses a prompted refresh by touching the file watched by our development scripts
|
||||
"""
|
||||
@@ -1494,6 +1521,12 @@ type Query {
|
||||
"""A user within the Cypress Cloud"""
|
||||
cloudViewer: CloudUser
|
||||
|
||||
"""Return the cohort for the given name"""
|
||||
cohort(
|
||||
"""the name of the cohort to find"""
|
||||
name: String!
|
||||
): Cohort
|
||||
|
||||
"""The currently opened project"""
|
||||
currentProject: CurrentProject
|
||||
|
||||
|
||||
19
packages/graphql/src/schemaTypes/objectTypes/gql-Cohorts.ts
Normal file
19
packages/graphql/src/schemaTypes/objectTypes/gql-Cohorts.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { inputObjectType, objectType } from 'nexus'
|
||||
|
||||
export const Cohort = objectType({
|
||||
name: 'Cohort',
|
||||
description: 'used to distinguish one group of users from another',
|
||||
definition (t) {
|
||||
t.nonNull.string('name', { description: 'name used to identify the cohort topic (e.g. "LoginBanner" ) ' })
|
||||
t.nonNull.string('cohort', { description: 'value used to indicate the cohort (e.g. "A" or "B")' })
|
||||
},
|
||||
})
|
||||
|
||||
export const CohortInput = inputObjectType({
|
||||
name: 'CohortInput',
|
||||
definition (t) {
|
||||
t.nonNull.string('name', { description: 'Name of the cohort' })
|
||||
t.nonNull.list.nonNull.string('cohorts', { description: 'Array of cohort options to choose from. Ex: A or B ' })
|
||||
t.list.nonNull.int('weights', { description: 'Optional array of integer weights to use for determining cohort. Defaults to even weighting' })
|
||||
},
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import { FileDetailsInput } from '../inputTypes/gql-FileDetailsInput'
|
||||
import { WizardUpdateInput } from '../inputTypes/gql-WizardUpdateInput'
|
||||
import { CurrentProject } from './gql-CurrentProject'
|
||||
import { GenerateSpecResponse } from './gql-GenerateSpecResponse'
|
||||
import { Cohort, CohortInput } from './gql-Cohorts'
|
||||
import { Query } from './gql-Query'
|
||||
import { ScaffoldedFile } from './gql-ScaffoldedFile'
|
||||
import { WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
@@ -684,6 +685,17 @@ export const mutation = mutationType({
|
||||
},
|
||||
})
|
||||
|
||||
t.field('determineCohort', {
|
||||
type: Cohort,
|
||||
description: 'Determine the cohort based on the given configuration. This will either return the cached cohort for a given name or choose a new one and store it.',
|
||||
args: {
|
||||
cohortConfig: nonNull(CohortInput),
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
return ctx.actions.cohorts.determineCohort(args.cohortConfig.name, args.cohortConfig.cohorts, args.cohortConfig.weights || undefined)
|
||||
},
|
||||
})
|
||||
|
||||
t.field('recordEvent', {
|
||||
type: 'Boolean',
|
||||
description: 'Dispatch an event to the dashboard to be recorded. Events are completely anonymous and are only used to identify aggregate usage patterns across all Cypress users.',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { idArg, nonNull, objectType } from 'nexus'
|
||||
import { idArg, stringArg, nonNull, objectType } from 'nexus'
|
||||
import { ProjectLike, ScaffoldedFile } from '..'
|
||||
import { CurrentProject } from './gql-CurrentProject'
|
||||
import { DevState } from './gql-DevState'
|
||||
@@ -9,6 +9,7 @@ import { VersionData } from './gql-VersionData'
|
||||
import { Wizard } from './gql-Wizard'
|
||||
import { ErrorWrapper } from './gql-ErrorWrapper'
|
||||
import { CachedUser } from './gql-CachedUser'
|
||||
import { Cohort } from './gql-Cohorts'
|
||||
|
||||
export const Query = objectType({
|
||||
name: 'Query',
|
||||
@@ -107,6 +108,17 @@ export const Query = objectType({
|
||||
resolve: (source, args, ctx) => Boolean(ctx.modeOptions.invokedFromCli),
|
||||
})
|
||||
|
||||
t.field('cohort', {
|
||||
description: 'Return the cohort for the given name',
|
||||
type: Cohort,
|
||||
args: {
|
||||
name: nonNull(stringArg({ description: 'the name of the cohort to find' })),
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
return await ctx.cohortsApi.getCohort(args.name) ?? null
|
||||
},
|
||||
})
|
||||
|
||||
t.field('node', {
|
||||
type: 'Node',
|
||||
args: {
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from './gql-Browser'
|
||||
export * from './gql-CachedUser'
|
||||
export * from './gql-CodeFrame'
|
||||
export * from './gql-CodeGenGlobs'
|
||||
export * from './gql-Cohorts'
|
||||
export * from './gql-CurrentProject'
|
||||
export * from './gql-DevState'
|
||||
export * from './gql-Editor'
|
||||
|
||||
@@ -36,8 +36,8 @@ const debug = null
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1756
|
||||
const zlibOptions = {
|
||||
flush: zlib.Z_SYNC_FLUSH,
|
||||
finishFlush: zlib.Z_SYNC_FLUSH,
|
||||
flush: zlib.constants.Z_SYNC_FLUSH,
|
||||
finishFlush: zlib.constants.Z_SYNC_FLUSH,
|
||||
}
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1543
|
||||
|
||||
@@ -94,6 +94,11 @@ export default class Attempt {
|
||||
addLog = (props: LogProps) => {
|
||||
switch (props.instrument) {
|
||||
case 'command': {
|
||||
// @ts-ignore satisfied by CommandProps
|
||||
if (props.sessionInfo) {
|
||||
this._addSession(props as unknown as SessionProps) // add sessionInstrumentPanel details
|
||||
}
|
||||
|
||||
return this._addCommand(props as CommandProps)
|
||||
}
|
||||
case 'agent': {
|
||||
@@ -112,6 +117,11 @@ export default class Attempt {
|
||||
const log = this._logs[props.id]
|
||||
|
||||
if (log) {
|
||||
// @ts-ignore satisfied by CommandProps
|
||||
if (props.sessionInfo) {
|
||||
this._updateOrAddSession(props as unknown as SessionProps) // update sessionInstrumentPanel details
|
||||
}
|
||||
|
||||
log.update(props)
|
||||
}
|
||||
}
|
||||
@@ -177,7 +187,19 @@ export default class Attempt {
|
||||
_addSession (props: SessionProps) {
|
||||
const session = new Session(props)
|
||||
|
||||
this.sessions[props.sessionInfo.id] = session
|
||||
this.sessions[props.id] = session
|
||||
}
|
||||
|
||||
_updateOrAddSession (props: SessionProps) {
|
||||
const session = this.sessions[props.id]
|
||||
|
||||
if (session) {
|
||||
session.update(props)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this._addSession(props)
|
||||
}
|
||||
|
||||
_addRoute (props: RouteProps) {
|
||||
|
||||
@@ -119,7 +119,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-indicator,
|
||||
.collapsible-more {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
margin-right: 8px;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 150ms ease-out;
|
||||
|
||||
.icon-dark {
|
||||
stroke: $gray-800;
|
||||
}
|
||||
}
|
||||
|
||||
.is-open > .collapsible-header-wrapper > .collapsible-header > .collapsible-header-inner > .collapsible-indicator {
|
||||
|
||||
@@ -4,9 +4,13 @@ import { action, computed, observable } from 'mobx'
|
||||
import Err, { ErrProps } from '../errors/err-model'
|
||||
import Instrument, { InstrumentProps } from '../instruments/instrument-model'
|
||||
import type { TimeoutID } from '../lib/types'
|
||||
import { SessionProps } from '../sessions/sessions-model'
|
||||
|
||||
const LONG_RUNNING_THRESHOLD = 1000
|
||||
|
||||
type InterceptStatuses = 'req modified' | 'req + res modified' | 'res modified'
|
||||
type XHRStatuses = '---' | '(canceled)' | '(aborted)' | string // string = any xhr status
|
||||
|
||||
export interface RenderProps {
|
||||
message?: string
|
||||
indicator?: 'successful' | 'pending' | 'aborted' | 'bad'
|
||||
@@ -15,20 +19,17 @@ export interface RenderProps {
|
||||
alias?: string
|
||||
type: 'function' | 'stub' | 'spy'
|
||||
}>
|
||||
status?: string
|
||||
status?: InterceptStatuses | XHRStatuses
|
||||
wentToOrigin?: boolean
|
||||
}
|
||||
|
||||
export interface SessionRenderProps {
|
||||
status: 'creating' | 'created' | 'restored' |'restored' | 'recreating' | 'recreated' | 'failed'
|
||||
}
|
||||
|
||||
export interface CommandProps extends InstrumentProps {
|
||||
err?: ErrProps
|
||||
event?: boolean
|
||||
number?: number
|
||||
numElements: number
|
||||
renderProps?: RenderProps | SessionRenderProps
|
||||
renderProps?: RenderProps
|
||||
sessionInfo?: SessionProps['sessionInfo']
|
||||
timeout?: number
|
||||
visible?: boolean
|
||||
wallClockStartedAt?: string
|
||||
@@ -43,6 +44,7 @@ export interface CommandProps extends InstrumentProps {
|
||||
|
||||
export default class Command extends Instrument {
|
||||
@observable.struct renderProps: RenderProps = {}
|
||||
@observable.struct sessionInfo?: SessionProps['sessionInfo']
|
||||
@observable err = new Err({})
|
||||
@observable event?: boolean = false
|
||||
@observable isLongRunning = false
|
||||
@@ -126,6 +128,7 @@ export default class Command extends Instrument {
|
||||
this.number = props.number
|
||||
this.numElements = props.numElements
|
||||
this.renderProps = props.renderProps || {}
|
||||
this.sessionInfo = props.sessionInfo
|
||||
this.timeout = props.timeout
|
||||
// command log that are not associated with elements will not have a visibility
|
||||
// attribute set. i.e. cy.visit(), cy.readFile() or cy.log()
|
||||
@@ -149,6 +152,7 @@ export default class Command extends Instrument {
|
||||
this.event = props.event
|
||||
this.numElements = props.numElements
|
||||
this.renderProps = props.renderProps || {}
|
||||
this.sessionInfo = props.sessionInfo
|
||||
// command log that are not associated with elements will not have a visibility
|
||||
// attribute set. i.e. cy.visit(), cy.readFile() or cy.log()
|
||||
this.visible = props.visible === undefined || props.visible
|
||||
|
||||
@@ -351,8 +351,8 @@ class Command extends Component<Props> {
|
||||
)}
|
||||
{isSessionCommand && (
|
||||
<Tag
|
||||
content={model.renderProps.status}
|
||||
type={`${model.renderProps.status === 'failed' ? 'failed' : 'successful'}-status`}
|
||||
content={model.sessionInfo?.status}
|
||||
type={`${model.sessionInfo?.status === 'failed' ? 'failed' : 'successful'}-status`}
|
||||
/>
|
||||
)}
|
||||
{!model.visible && (
|
||||
|
||||
@@ -3,12 +3,7 @@ import { action, computed, observable } from 'mobx'
|
||||
import { TestState } from '../test/test-model'
|
||||
import { IntervalID } from '../lib/types'
|
||||
|
||||
export interface StatsStoreStartInfo {
|
||||
startTime: string
|
||||
numPassed?: number
|
||||
numFailed?: number
|
||||
numPending?: number
|
||||
}
|
||||
import type { StatsStoreStartInfo } from '@packages/types'
|
||||
|
||||
const defaults = {
|
||||
numPassed: 0,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
border-right: 1px solid $reporter-section-background;
|
||||
margin-bottom: 10px;
|
||||
overflow: auto;
|
||||
padding: 3px 10px;
|
||||
padding: 0 2px 0 12px;
|
||||
}
|
||||
|
||||
.instrument-content h3,
|
||||
|
||||
@@ -2,10 +2,11 @@ import { EventEmitter } from 'events'
|
||||
import { action } from 'mobx'
|
||||
import appState, { AppState } from './app-state'
|
||||
import runnablesStore, { RunnablesStore, RootRunnable, LogProps } from '../runnables/runnables-store'
|
||||
import statsStore, { StatsStore, StatsStoreStartInfo } from '../header/stats-store'
|
||||
import statsStore, { StatsStore } from '../header/stats-store'
|
||||
import scroller, { Scroller } from './scroller'
|
||||
import TestModel, { UpdatableTestProps, UpdateTestCallback, TestProps } from '../test/test-model'
|
||||
import { SessionProps } from '../sessions/sessions-model'
|
||||
|
||||
import type { ReporterStartInfo, ReporterRunState } from '@packages/types'
|
||||
|
||||
const localBus = new EventEmitter()
|
||||
|
||||
@@ -33,16 +34,7 @@ export interface Events {
|
||||
__off: (() => void)
|
||||
}
|
||||
|
||||
interface StartInfo extends StatsStoreStartInfo {
|
||||
autoScrollingEnabled: boolean
|
||||
scrollTop: number
|
||||
studioActive: boolean
|
||||
}
|
||||
|
||||
type CollectRunStateCallback = (arg: {
|
||||
autoScrollingEnabled: boolean
|
||||
scrollTop: number
|
||||
}) => void
|
||||
type CollectRunStateCallback = (arg: ReporterRunState) => void
|
||||
|
||||
const events: Events = {
|
||||
appState,
|
||||
@@ -72,10 +64,6 @@ const events: Events = {
|
||||
runnablesStore.updateLog(log)
|
||||
}))
|
||||
|
||||
runner.on('session:add', action('session:add', (props: SessionProps) => {
|
||||
runnablesStore._withTest(props.testId, (test) => test.addSession(props))
|
||||
}))
|
||||
|
||||
runner.on('reporter:log:remove', action('log:remove', (log: LogProps) => {
|
||||
runnablesStore.removeLog(log)
|
||||
}))
|
||||
@@ -93,7 +81,7 @@ const events: Events = {
|
||||
}
|
||||
}))
|
||||
|
||||
runner.on('reporter:start', action('start', (startInfo: StartInfo) => {
|
||||
runner.on('reporter:start', action('start', (startInfo: ReporterStartInfo) => {
|
||||
appState.temporarilySetAutoScrolling(startInfo.autoScrollingEnabled)
|
||||
runnablesStore.setInitialScrollTop(startInfo.scrollTop)
|
||||
appState.setStudioActive(startInfo.studioActive)
|
||||
@@ -193,8 +181,8 @@ const events: Events = {
|
||||
runner.emit('get:user:editor', cb)
|
||||
})
|
||||
|
||||
localBus.on('clear:session', (cb) => {
|
||||
runner.emit('clear:session', cb)
|
||||
localBus.on('clear:all:sessions', (cb) => {
|
||||
runner.emit('clear:all:sessions', cb)
|
||||
})
|
||||
|
||||
localBus.on('set:user:editor', (editor) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: inherit;
|
||||
line-height: initial;
|
||||
height: 18px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
@@ -62,6 +62,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.session-item:hover > .session-tag > .reporter-tag.successful-status,
|
||||
.command-wrapper:hover > .command-pin-target > .command-wrapper-text > .command-controls > .reporter-tag.successful-status {
|
||||
border-color: $gray-700;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import RouteModel, { RouteProps } from '../routes/route-model'
|
||||
import TestModel, { TestProps, UpdatableTestProps, UpdateTestCallback } from '../test/test-model'
|
||||
import RunnableModel from './runnable-model'
|
||||
import SuiteModel, { SuiteProps } from './suite-model'
|
||||
import { SessionProps } from '../sessions/sessions-model'
|
||||
|
||||
const defaults = {
|
||||
hasSingleTest: false,
|
||||
@@ -25,7 +24,7 @@ interface Props {
|
||||
scroller: Scroller
|
||||
}
|
||||
|
||||
export type LogProps = AgentProps | CommandProps | RouteProps | SessionProps
|
||||
export type LogProps = AgentProps | CommandProps | RouteProps
|
||||
|
||||
export type RunnableArray = Array<TestModel | SuiteModel>
|
||||
|
||||
@@ -44,11 +43,11 @@ export class RunnablesStore {
|
||||
@observable isReady = defaults.isReady
|
||||
@observable runnables: RunnableArray = []
|
||||
/**
|
||||
* Stores a list of all the runables files where the reporter
|
||||
* Stores a list of all the runnables files where the reporter
|
||||
* has passed without any specific order.
|
||||
*
|
||||
* key: spec FilePath
|
||||
* content: RunableArray
|
||||
* content: RunnableArray
|
||||
*/
|
||||
@observable runnablesHistory: Record<string, RunnableArray> = {}
|
||||
|
||||
@@ -62,7 +61,6 @@ export class RunnablesStore {
|
||||
[key: string]: any
|
||||
|
||||
_tests: Record<string, TestModel> = {}
|
||||
_logs: Record<string, Log> = {}
|
||||
_runnablesQueue: Array<RunnableModel> = []
|
||||
|
||||
attemptingShowSnapshot = defaults.attemptingShowSnapshot
|
||||
@@ -162,9 +160,9 @@ export class RunnablesStore {
|
||||
return this._tests[id]
|
||||
}
|
||||
|
||||
addLog (log: LogProps) {
|
||||
this._withTest(log.testId, (test) => {
|
||||
test.addLog(log)
|
||||
addLog (props: LogProps) {
|
||||
this._withTest(props.testId, (test) => {
|
||||
test.addLog(props)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,29 @@ export interface SessionProps extends InstrumentProps {
|
||||
testCurrentRetry: number
|
||||
sessionInfo: {
|
||||
id: string
|
||||
data: Record<string, {cookies: number, localStorage: number}>
|
||||
isGlobalSession: boolean
|
||||
status: 'creating' | 'created' | 'restored' |'restored' | 'recreating' | 'recreated' | 'failed'
|
||||
}
|
||||
}
|
||||
|
||||
export default class Session extends Instrument {
|
||||
@observable name: string
|
||||
@observable data: SessionProps['sessionInfo']['data']
|
||||
@observable status: string
|
||||
@observable isGlobalSession: boolean = false
|
||||
|
||||
constructor (props: SessionProps) {
|
||||
super(props)
|
||||
const { id, data } = props.sessionInfo
|
||||
const { sessionInfo: { isGlobalSession, id, status } } = props
|
||||
|
||||
this.isGlobalSession = isGlobalSession
|
||||
this.name = id
|
||||
this.data = { ...data }
|
||||
this.status = status
|
||||
}
|
||||
|
||||
update (props: Partial<SessionProps>) {
|
||||
const { sessionInfo, state } = props
|
||||
|
||||
this.status = sessionInfo?.status || ''
|
||||
this.state = state || ''
|
||||
}
|
||||
}
|
||||
|
||||
146
packages/reporter/src/sessions/sessions.cy.tsx
Normal file
146
packages/reporter/src/sessions/sessions.cy.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React from 'react'
|
||||
import Sessions from './sessions'
|
||||
import SessionsModel from './sessions-model'
|
||||
import events from '../lib/events'
|
||||
|
||||
describe('sessions instrument panel', { viewportWidth: 400 }, () => {
|
||||
it('renders null when no sessions have been added', () => {
|
||||
cy.mount(<Sessions model={[]}/>)
|
||||
|
||||
cy.get('.sessions-container').should('not.exist')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
describe('renders with sessions', () => {
|
||||
const specSession = new SessionsModel({
|
||||
name: 'session',
|
||||
state: 'passed',
|
||||
type: 'parent',
|
||||
testId: '1',
|
||||
id: 1,
|
||||
testCurrentRetry: 1,
|
||||
sessionInfo: {
|
||||
id: 'spec_session',
|
||||
isGlobalSession: false,
|
||||
status: 'created',
|
||||
},
|
||||
})
|
||||
|
||||
const globalSession = new SessionsModel({
|
||||
name: 'session',
|
||||
state: 'passed',
|
||||
type: 'parent',
|
||||
testId: '1',
|
||||
id: 2,
|
||||
testCurrentRetry: 1,
|
||||
sessionInfo: {
|
||||
id: 'global_session',
|
||||
isGlobalSession: true,
|
||||
status: 'restored',
|
||||
},
|
||||
})
|
||||
|
||||
const failedSpecSession = new SessionsModel({
|
||||
name: 'session',
|
||||
state: 'failed',
|
||||
type: 'parent',
|
||||
testId: '1',
|
||||
id: 3,
|
||||
sessionInfo: {
|
||||
id: 'spec_session_failed',
|
||||
isGlobalSession: false,
|
||||
status: 'failed',
|
||||
},
|
||||
testCurrentRetry: 1,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mount(<Sessions model={[specSession, globalSession, failedSpecSession]}/>)
|
||||
|
||||
cy.get('.sessions-container').should('exist')
|
||||
cy.get('.hook-header > .collapsible-header').as('header')
|
||||
cy.contains('Clear All Sessions')
|
||||
})
|
||||
|
||||
it('is collapsed by default', () => {
|
||||
cy.get('@header').should('have.attr', 'aria-expanded', 'false')
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('opens/closes when clicking panel', () => {
|
||||
cy.get('@header').click()
|
||||
|
||||
cy.get('@header').should('have.attr', 'aria-expanded', 'true')
|
||||
|
||||
cy.get('.session-item')
|
||||
.should('have.length', 3)
|
||||
.should('be.visible')
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('has spec session item', () => {
|
||||
cy.get('@header').click()
|
||||
|
||||
cy.get('.session-item').eq(0)
|
||||
.within(() => {
|
||||
cy.contains('spec_session').should('have.class', 'spec-session')
|
||||
cy.get('.global-session-icon').should('not.exist')
|
||||
cy.get('.session-status').should('have.class', 'successful-status')
|
||||
})
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('has global session item', () => {
|
||||
cy.get('@header').click()
|
||||
|
||||
cy.get('.session-item').eq(1)
|
||||
.within(() => {
|
||||
cy.contains('global_session').should('not.have.class', 'spec-session')
|
||||
cy.get('.global-session-icon').should('exist')
|
||||
cy.get('.session-status').should('have.class', 'successful-status')
|
||||
})
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('has failed session item', () => {
|
||||
cy.get('@header').click()
|
||||
|
||||
cy.get('.session-item')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.contains('spec_session_failed').should('have.class', 'spec-session')
|
||||
cy.get('.global-session-icon').should('not.exist')
|
||||
cy.get('.session-status').should('have.class', 'failed-status')
|
||||
})
|
||||
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('clicking session item prints details to the console', () => {
|
||||
cy.spy(events, 'emit')
|
||||
|
||||
cy.get('@header').click()
|
||||
|
||||
cy.get('.session-item').eq(2).click()
|
||||
|
||||
cy.get('.cy-tooltip')
|
||||
.should('have.text', 'Printed output to your console')
|
||||
.then(() => {
|
||||
expect(events.emit).to.be.calledWith('show:command', failedSpecSession.testId, failedSpecSession.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('clicking "Clear All Sessions" button emits "clear:all:sessions" event', () => {
|
||||
cy.spy(events, 'emit')
|
||||
|
||||
cy.contains('Clear All Sessions').click()
|
||||
.then(() => {
|
||||
expect(events.emit).to.be.calledWith('clear:all:sessions')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,10 @@
|
||||
.sessions-container.instruments-container {
|
||||
.instrument-content {
|
||||
padding: 0;
|
||||
.sessions-container {
|
||||
.session-content {
|
||||
font-family: $monospace;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.clear-sessions {
|
||||
@@ -8,24 +12,55 @@
|
||||
color: $gray-400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.session-item {
|
||||
padding: 3px 10px;
|
||||
|
||||
background-color: $gray-1000;
|
||||
color: $gray-400;
|
||||
min-height: 20px;
|
||||
.session-content .session-item-wrapper {
|
||||
display: block;
|
||||
margin-top: -1px;
|
||||
padding: 0 2px 0 12px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $gray-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-content .session-item-wrapper:last-child {
|
||||
.session-item {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.session-item {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba($gray-900, 0.4);
|
||||
display: flex;
|
||||
color: $gray-400;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
min-height: 29px;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
padding-left: 8px;
|
||||
|
||||
&.spec-session {
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
.session-tag {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.global-session-icon {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +1,76 @@
|
||||
import _ from 'lodash'
|
||||
import cs from 'classnames'
|
||||
import React from 'react'
|
||||
import { observer } from 'mobx-react'
|
||||
import GlobeIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/globe_x12.svg'
|
||||
|
||||
import events, { Events } from '../lib/events'
|
||||
import SessionsModel from './sessions-model'
|
||||
import events from '../lib/events'
|
||||
import Collapsible from '../collapsible/collapsible'
|
||||
import Tag from '../lib/tag'
|
||||
import FlashOnClick from '../lib/flash-on-click'
|
||||
|
||||
export interface SessionsProps {
|
||||
export interface SessionPanelProps {
|
||||
model: Record<string, SessionsModel>
|
||||
events: Events
|
||||
}
|
||||
|
||||
@observer
|
||||
class Sessions extends React.Component<SessionsProps> {
|
||||
static defaultProps = {
|
||||
events,
|
||||
const SessionRow = ({ name, isGlobalSession, id, state, status, testId }: SessionsModel) => {
|
||||
const printToConsole = (id) => {
|
||||
events.emit('show:command', testId, id)
|
||||
}
|
||||
|
||||
printToConsole = (name) => {
|
||||
const { id, testId } = this.props.model[name]
|
||||
|
||||
this.props.events.emit('show:command', testId, id)
|
||||
}
|
||||
|
||||
render () {
|
||||
const model = this.props.model
|
||||
|
||||
if (!_.size(model)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cs('runnable-agents-region', {
|
||||
'no-agents': !_.size(model),
|
||||
})}
|
||||
>
|
||||
|
||||
<div className='instruments-container sessions-container'>
|
||||
<ul className='hooks-container'>
|
||||
<li className='hook-item'>
|
||||
<Collapsible
|
||||
header={<>
|
||||
Sessions <i style={{ textTransform: 'none' }}>({_.size(model)})</i>
|
||||
</>
|
||||
}
|
||||
headerClass='hook-header'
|
||||
headerExtras={
|
||||
<div className="clear-sessions"
|
||||
onClick={() => events.emit('clear:session')}
|
||||
><span><i className="fas fa-ban" /> Clear All Sessions</span></div>}
|
||||
contentClass='instrument-content'
|
||||
>
|
||||
<div>
|
||||
{_.map(model, (sess) => {
|
||||
return (<FlashOnClick
|
||||
key={sess.name}
|
||||
message='Printed output to your console'
|
||||
onClick={() => this.printToConsole(sess.name)}
|
||||
shouldShowMessage={() => true}
|
||||
><div className="session-item" >{sess.name}</div></FlashOnClick>)
|
||||
})}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
return (
|
||||
<FlashOnClick
|
||||
key={name}
|
||||
message='Printed output to your console'
|
||||
onClick={() => printToConsole(id)}
|
||||
shouldShowMessage={() => true}
|
||||
wrapperClassName='session-item-wrapper'
|
||||
>
|
||||
<div className='session-item'>
|
||||
<span className={cs('session-info', { 'spec-session': !isGlobalSession })}>
|
||||
{isGlobalSession && <GlobeIcon className='global-session-icon' />}
|
||||
{name}
|
||||
</span>
|
||||
<span className='session-tag'>
|
||||
<Tag
|
||||
customClassName='session-status'
|
||||
content={status}
|
||||
type={`${state === 'failed' ? 'failed' : 'successful'}-status`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</FlashOnClick>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sessions
|
||||
const Sessions = ({ model }: SessionPanelProps) => {
|
||||
const sessions = Object.values(model)
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className='instruments-container hooks-container sessions-container'>
|
||||
<li className='hook-item'>
|
||||
<Collapsible
|
||||
header={<>Sessions <i style={{ textTransform: 'none' }}>({sessions.length})</i></>}
|
||||
headerClass='hook-header'
|
||||
headerExtras={
|
||||
<div
|
||||
className="clear-sessions"
|
||||
onClick={() => events.emit('clear:all:sessions')}
|
||||
>
|
||||
<span><i className="fas fa-ban" /> Clear All Sessions</span>
|
||||
</div>
|
||||
}
|
||||
contentClass='instrument-content session-content'
|
||||
>
|
||||
{sessions.map((session) => (<SessionRow key={session.id} {...session} />))}
|
||||
</Collapsible>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(Sessions)
|
||||
|
||||
@@ -10,7 +10,6 @@ import { CommandProps } from '../commands/command-model'
|
||||
import { AgentProps } from '../agents/agent-model'
|
||||
import { RouteProps } from '../routes/route-model'
|
||||
import { RunnablesStore, LogProps } from '../runnables/runnables-store'
|
||||
import { SessionProps } from '../sessions/sessions-model'
|
||||
|
||||
export type UpdateTestCallback = () => void
|
||||
|
||||
@@ -128,10 +127,6 @@ export default class Test extends Runnable {
|
||||
})
|
||||
}
|
||||
|
||||
addSession (props: SessionProps) {
|
||||
return this._withAttempt(props.testCurrentRetry, (attempt) => attempt._addSession(props))
|
||||
}
|
||||
|
||||
updateLog (props: LogProps) {
|
||||
this._withAttempt(props.testCurrentRetry || this.currentRetry, (attempt: Attempt) => {
|
||||
attempt.updateLog(props)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// Studio tests have been removed with v10 update.
|
||||
// You can find the tests in the PR below.
|
||||
// @see https://github.com/cypress-io/cypress/pull/9542
|
||||
|
||||
export * from './studio-recorder'
|
||||
@@ -45,6 +45,7 @@ module.exports = {
|
||||
PROJECTS: [],
|
||||
PROJECT_PREFERENCES: {},
|
||||
PROJECTS_CONFIG: {},
|
||||
COHORTS: {},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -184,6 +185,21 @@ module.exports = {
|
||||
return fileUtil.set({ PROJECT_PREFERENCES: updatedPreferences })
|
||||
},
|
||||
|
||||
getCohorts () {
|
||||
return fileUtil.get('COHORTS', {})
|
||||
},
|
||||
|
||||
insertCohort (cohort) {
|
||||
return fileUtil.transaction((tx) => {
|
||||
return tx.get('COHORTS', {}).then((cohorts) => {
|
||||
return tx.set('COHORTS', {
|
||||
...cohorts,
|
||||
[cohort.name]: cohort,
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
remove () {
|
||||
return fileUtil.remove()
|
||||
},
|
||||
|
||||
25
packages/server/lib/cohorts.ts
Normal file
25
packages/server/lib/cohorts.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const cache = require('./cache')
|
||||
import type { Cohort } from '@packages/types'
|
||||
const debug = require('debug')('cypress:server:cohorts')
|
||||
|
||||
export = {
|
||||
get: (): Promise<Record<string, Cohort>> => {
|
||||
debug('Get cohorts')
|
||||
|
||||
return cache.getCohorts()
|
||||
},
|
||||
getByName: (name: string): Promise<Cohort> => {
|
||||
debug('Get cohort name:', name)
|
||||
|
||||
return cache.getCohorts().then((cohorts) => {
|
||||
debug('Get cohort returning:', cohorts[name])
|
||||
|
||||
return cohorts[name]
|
||||
})
|
||||
},
|
||||
set: (cohort: Cohort) => {
|
||||
debug('Set cohort', cohort)
|
||||
|
||||
return cache.insertCohort(cohort)
|
||||
},
|
||||
}
|
||||
@@ -2,10 +2,6 @@ import _ from 'lodash'
|
||||
import type { ResolvedFromConfig } from '@packages/types'
|
||||
import * as configUtils from '@packages/config'
|
||||
|
||||
export const setupFullConfigWithDefaults = configUtils.setupFullConfigWithDefaults
|
||||
|
||||
export const updateWithPluginValues = configUtils.updateWithPluginValues
|
||||
|
||||
export const setUrls = configUtils.setUrls
|
||||
|
||||
export function getResolvedRuntimeConfig (config, runtimeConfig) {
|
||||
|
||||
@@ -140,7 +140,7 @@ export const getExperiments = (project: CypressProject, names = experimental.nam
|
||||
}
|
||||
|
||||
/**
|
||||
* Whilelist known experiments here to avoid accidentally showing
|
||||
* Allow known experiments here to avoid accidentally showing
|
||||
* any config key that starts with "experimental" prefix
|
||||
*/
|
||||
// @ts-ignore
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
import browserUtils from './browsers/utils'
|
||||
import auth from './gui/auth'
|
||||
import user from './user'
|
||||
import cohorts from './cohorts'
|
||||
import { openProject } from './open_project'
|
||||
import cache from './cache'
|
||||
import { graphqlSchema } from '@packages/graphql/src/schema'
|
||||
@@ -195,6 +196,17 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
return availableEditors
|
||||
},
|
||||
},
|
||||
cohortsApi: {
|
||||
async getCohorts () {
|
||||
return cohorts.get()
|
||||
},
|
||||
async getCohort (name: string) {
|
||||
return cohorts.getByName(name)
|
||||
},
|
||||
async insertCohort (cohort) {
|
||||
cohorts.set(cohort)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -718,7 +718,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea
|
||||
|
||||
async function runEachSpec (spec: SpecWithRelativeRoot, index: number, length: number, estimated: number) {
|
||||
if (!options.quiet) {
|
||||
printResults.displaySpecHeader(spec.baseName, index + 1, length, estimated)
|
||||
printResults.displaySpecHeader(spec.relativeToCommonRoot, index + 1, length, estimated)
|
||||
}
|
||||
|
||||
const { results } = await runSpec(config, spec, options, estimated, isFirstSpec, index === length - 1)
|
||||
|
||||
@@ -1,33 +1,47 @@
|
||||
import type { CyCookie } from './browsers/cdp_automation'
|
||||
import type { ServerSessionData, StoredSessions } from '@packages/types'
|
||||
|
||||
interface SessionData {
|
||||
cookies: CyCookie[]
|
||||
id: string
|
||||
localStorage: object
|
||||
sessionStorage: object
|
||||
}
|
||||
const state = {
|
||||
sessions: {},
|
||||
type State = {
|
||||
globalSessions: StoredSessions
|
||||
specSessions: StoredSessions
|
||||
}
|
||||
|
||||
export function saveSession (data: SessionData) {
|
||||
const state: State = {
|
||||
globalSessions: {},
|
||||
specSessions: {},
|
||||
}
|
||||
|
||||
export function saveSession (data: ServerSessionData): void {
|
||||
if (!data.id) throw new Error('session data had no id')
|
||||
|
||||
state.sessions[data.id] = data
|
||||
if (data.cacheAcrossSpecs) {
|
||||
state.globalSessions[data.id] = data
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
state.specSessions[data.id] = data
|
||||
}
|
||||
|
||||
export function getSession (id: string): SessionData {
|
||||
const session = state.sessions[id]
|
||||
export function getActiveSessions (): StoredSessions {
|
||||
return state.globalSessions
|
||||
}
|
||||
|
||||
export function getSession (id: string): ServerSessionData {
|
||||
const session = state.globalSessions[id] || state.specSessions[id]
|
||||
|
||||
if (!session) throw new Error(`session with id "${id}" not found`)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
export function getState () {
|
||||
export function getState (): State {
|
||||
return state
|
||||
}
|
||||
|
||||
export function clearSessions () {
|
||||
state.sessions = {}
|
||||
export function clearSessions (clearAllSessions: boolean = false): void {
|
||||
state.specSessions = {}
|
||||
|
||||
if (clearAllSessions) {
|
||||
state.globalSessions = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +26,13 @@ import runEvents from './plugins/run_events'
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import type { Socket } from '@packages/socket'
|
||||
|
||||
import type { RunState, CachedTestState } from '@packages/types'
|
||||
|
||||
type StartListeningCallbacks = {
|
||||
onSocketConnection: (socket: any) => void
|
||||
}
|
||||
|
||||
type RunnerEvent =
|
||||
'reporter:restart:test:run'
|
||||
| 'runnables:ready'
|
||||
| 'run:start'
|
||||
| 'test:before:run:async'
|
||||
| 'reporter:log:add'
|
||||
| 'reporter:log:state:changed'
|
||||
| 'paused'
|
||||
| 'test:after:hooks'
|
||||
| 'run:end'
|
||||
|
||||
const runnerEvents: RunnerEvent[] = [
|
||||
const runnerEvents = [
|
||||
'reporter:restart:test:run',
|
||||
'runnables:ready',
|
||||
'run:start',
|
||||
@@ -51,18 +42,9 @@ const runnerEvents: RunnerEvent[] = [
|
||||
'paused',
|
||||
'test:after:hooks',
|
||||
'run:end',
|
||||
]
|
||||
] as const
|
||||
|
||||
type ReporterEvent =
|
||||
'runner:restart'
|
||||
| 'runner:abort'
|
||||
| 'runner:console:log'
|
||||
| 'runner:console:error'
|
||||
| 'runner:show:snapshot'
|
||||
| 'runner:hide:snapshot'
|
||||
| 'reporter:restarted'
|
||||
|
||||
const reporterEvents: ReporterEvent[] = [
|
||||
const reporterEvents = [
|
||||
// "go:to:file"
|
||||
'runner:restart',
|
||||
'runner:abort',
|
||||
@@ -71,7 +53,7 @@ const reporterEvents: ReporterEvent[] = [
|
||||
'runner:show:snapshot',
|
||||
'runner:hide:snapshot',
|
||||
'reporter:restarted',
|
||||
]
|
||||
] as const
|
||||
|
||||
const debug = Debug('cypress:server:socket-base')
|
||||
|
||||
@@ -156,7 +138,7 @@ export class SocketBase {
|
||||
options,
|
||||
callbacks: StartListeningCallbacks,
|
||||
) {
|
||||
let existingState = null
|
||||
let runState: RunState | undefined = undefined
|
||||
|
||||
_.defaults(options, {
|
||||
socketId: null,
|
||||
@@ -350,7 +332,6 @@ export class SocketBase {
|
||||
})
|
||||
|
||||
// TODO: what to do about runner disconnections?
|
||||
|
||||
socket.on('spec:changed', (spec) => {
|
||||
return options.onSpecChanged(spec)
|
||||
})
|
||||
@@ -402,8 +383,7 @@ export class SocketBase {
|
||||
})
|
||||
}
|
||||
|
||||
// retry for up to data.timeout
|
||||
// or 1 second
|
||||
// retry for up to data.timeout or 1 second
|
||||
return Bluebird
|
||||
.try(tryConnected)
|
||||
.timeout(data.timeout != null ? data.timeout : 1000)
|
||||
@@ -428,7 +408,7 @@ export class SocketBase {
|
||||
|
||||
switch (eventName) {
|
||||
case 'preserve:run:state':
|
||||
existingState = args[0]
|
||||
runState = args[0]
|
||||
|
||||
return null
|
||||
case 'resolve:url': {
|
||||
@@ -467,21 +447,20 @@ export class SocketBase {
|
||||
return task.run(cfgFile ?? null, args[0])
|
||||
case 'save:session':
|
||||
return session.saveSession(args[0])
|
||||
case 'clear:session':
|
||||
return session.clearSessions()
|
||||
case 'clear:sessions':
|
||||
return session.clearSessions(args[0])
|
||||
case 'get:session':
|
||||
return session.getSession(args[0])
|
||||
case 'reset:session:state':
|
||||
case 'reset:cached:test:state':
|
||||
runState = undefined
|
||||
cookieJar.removeAllCookies()
|
||||
session.clearSessions()
|
||||
resetRenderedHTMLOrigins()
|
||||
|
||||
return
|
||||
return resetRenderedHTMLOrigins()
|
||||
case 'get:rendered:html:origins':
|
||||
return options.getRenderedHTMLOrigins()
|
||||
case 'reset:rendered:html:origins': {
|
||||
case 'reset:rendered:html:origins':
|
||||
return resetRenderedHTMLOrigins()
|
||||
}
|
||||
case 'cross:origin:automation:cookies:received':
|
||||
return this.localBus.emit('cross:origin:automation:cookies:received')
|
||||
case 'request:sent:with:credentials':
|
||||
@@ -500,16 +479,18 @@ export class SocketBase {
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('get:existing:run:state', (cb) => {
|
||||
const s = existingState
|
||||
socket.on('get:cached:test:state', (cb: (runState: RunState | null, testState: CachedTestState) => void) => {
|
||||
const s = runState
|
||||
|
||||
if (s) {
|
||||
existingState = null
|
||||
|
||||
return cb(s)
|
||||
const cachedTestState: CachedTestState = {
|
||||
activeSessions: session.getActiveSessions(),
|
||||
}
|
||||
|
||||
return cb()
|
||||
if (s) {
|
||||
runState = undefined
|
||||
}
|
||||
|
||||
return cb(s || {}, cachedTestState)
|
||||
})
|
||||
|
||||
socket.on('save:app:state', (state, cb) => {
|
||||
@@ -550,7 +531,7 @@ export class SocketBase {
|
||||
// todo(lachlan): post 10.0 we should not pass the
|
||||
// editor (in the `fileDetails.where` key) from the
|
||||
// front-end, but rather rely on the server context
|
||||
// to grab the prefered editor, like I'm doing here,
|
||||
// to grab the preferred editor, like I'm doing here,
|
||||
// so we do not need to
|
||||
// maintain two sources of truth for the preferred editor
|
||||
// adding this conditional to maintain backwards compat with
|
||||
|
||||
@@ -113,7 +113,7 @@ function macOSRemovePrivate (str: string) {
|
||||
function collectTestResults (obj: { video?: boolean, screenshots?: Screenshot[], spec?: any, stats?: any }, estimated: number) {
|
||||
return {
|
||||
name: _.get(obj, 'spec.name'),
|
||||
baseName: _.get(obj, 'spec.baseName'),
|
||||
relativeToCommonRoot: _.get(obj, 'spec.relativeToCommonRoot'),
|
||||
tests: _.get(obj, 'stats.tests'),
|
||||
passes: _.get(obj, 'stats.passes'),
|
||||
pending: _.get(obj, 'stats.pending'),
|
||||
@@ -203,7 +203,7 @@ export function displayRunStarting (options: { browser: Browser, config: Cfg, gr
|
||||
|
||||
const formatSpecs = (specs) => {
|
||||
// 25 found: (foo.spec.js, bar.spec.js, baz.spec.js)
|
||||
const names = _.map(specs, 'baseName')
|
||||
const names = _.map(specs, 'relativeToCommonRoot')
|
||||
const specsTruncated = _.truncate(names.join(', '), { length: 250 })
|
||||
|
||||
const stringifiedSpecs = [
|
||||
@@ -325,7 +325,7 @@ export function renderSummaryTable (runUrl: string | undefined, results: any) {
|
||||
|
||||
const ms = duration.format(stats.wallClockDuration || 0)
|
||||
|
||||
const formattedSpec = formatPath(spec.baseName, getWidth(table2, 1))
|
||||
const formattedSpec = formatPath(spec.relativeToCommonRoot, getWidth(table2, 1))
|
||||
|
||||
if (run.skippedSpec) {
|
||||
return table2.push([
|
||||
@@ -398,7 +398,7 @@ export function displayResults (obj: { screenshots?: Screenshot[] }, estimated:
|
||||
['Video:', results.video],
|
||||
['Duration:', results.duration],
|
||||
estimated ? ['Estimated:', results.estimated] : undefined,
|
||||
['Spec Ran:', formatPath(results.baseName, getWidth(table, 1), c)],
|
||||
['Spec Ran:', formatPath(results.relativeToCommonRoot, getWidth(table, 1), c)],
|
||||
])
|
||||
.compact()
|
||||
.map((arr) => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||||
const SseStream = require('ssestream')
|
||||
const EventSource = require('eventsource')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const pluginsModule = require(`../../lib/plugins`)
|
||||
@@ -107,7 +108,7 @@ describe('Routes', () => {
|
||||
// get all the config defaults
|
||||
// and allow us to override them
|
||||
// for each test
|
||||
return config.setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
// use a jar for each test
|
||||
// but reset it automatically
|
||||
|
||||
@@ -5,6 +5,7 @@ const http = require('http')
|
||||
const rp = require('@cypress/request-promise')
|
||||
const Promise = require('bluebird')
|
||||
const evilDns = require('evil-dns')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
@@ -47,7 +48,7 @@ describe('Server', () => {
|
||||
// get all the config defaults
|
||||
// and allow us to override them
|
||||
// for each test
|
||||
return config.setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
// use a jar for each test
|
||||
// but reset it automatically
|
||||
@@ -102,8 +103,6 @@ describe('Server', () => {
|
||||
|
||||
this.srv = this.server.getHttpServer()
|
||||
|
||||
// @session = new (Session({app: @srv}))
|
||||
|
||||
this.proxy = `http://localhost:${port}`
|
||||
|
||||
this.buffers = this.server._networkProxy.http.buffers
|
||||
|
||||
@@ -21,7 +21,7 @@ const { createRoutes } = require(`../../lib/routes`)
|
||||
process.env.CYPRESS_INTERNAL_ENV = 'development'
|
||||
|
||||
const CA = require('@packages/https-proxy').CA
|
||||
const Config = require('../../lib/config')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { _getArgs } = require('../../lib/browsers/chrome')
|
||||
@@ -350,7 +350,7 @@ describe('Proxy Performance', function () {
|
||||
https: { cert, key },
|
||||
}).start(HTTPS_PROXY_PORT),
|
||||
|
||||
Config.setupFullConfigWithDefaults({
|
||||
setupFullConfigWithDefaults({
|
||||
projectRoot: '/tmp/a',
|
||||
config: {
|
||||
supportFile: false,
|
||||
|
||||
@@ -256,6 +256,28 @@ describe('lib/cache', () => {
|
||||
PROJECTS: ['foo'],
|
||||
PROJECT_PREFERENCES: {},
|
||||
PROJECTS_CONFIG: {},
|
||||
COHORTS: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('cohorts', () => {
|
||||
it('should get no cohorts when empty', () => {
|
||||
return cache.getCohorts().then((cohorts) => {
|
||||
expect(cohorts).to.deep.eq({})
|
||||
})
|
||||
})
|
||||
|
||||
it('should insert a cohort', () => {
|
||||
const cohort = {
|
||||
name: 'cohort_id',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
return cache.insertCohort(cohort).then(() => {
|
||||
return cache.getCohorts().then((cohorts) => {
|
||||
expect(cohorts).to.deep.eq({ [cohort.name]: cohort })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
63
packages/server/test/unit/cohort_spec.ts
Normal file
63
packages/server/test/unit/cohort_spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
require('../spec_helper')
|
||||
|
||||
import { Cohort } from '@packages/types'
|
||||
import cache from '../../lib/cache'
|
||||
import cohorts from '../../lib/cohorts'
|
||||
|
||||
describe('lib/cohort', () => {
|
||||
context('.get', () => {
|
||||
it('calls cache.get', async () => {
|
||||
const cohortTest: Cohort = {
|
||||
name: 'testName',
|
||||
cohort: 'A',
|
||||
}
|
||||
const cohortTest2: Cohort = {
|
||||
name: 'testName2',
|
||||
cohort: 'B',
|
||||
}
|
||||
|
||||
const allCohorts = {
|
||||
[cohortTest.name]: cohortTest,
|
||||
[cohortTest2.name]: cohortTest2,
|
||||
}
|
||||
|
||||
sinon.stub(cache, 'getCohorts').resolves(allCohorts)
|
||||
|
||||
return cohorts.get().then((cohorts) => {
|
||||
expect(cohorts).to.eq(allCohorts)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.getByName', () => {
|
||||
it('calls cache.getByName', async () => {
|
||||
const cohortTest: Cohort = {
|
||||
name: 'testName',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
sinon.stub(cache, 'getCohorts').resolves({
|
||||
[cohortTest.name]: cohortTest,
|
||||
})
|
||||
|
||||
return cohorts.getByName(cohortTest.name).then((cohort) => {
|
||||
expect(cohort).to.eq(cohortTest)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.set', () => {
|
||||
it('calls cache.set', async () => {
|
||||
const cohortTest: Cohort = {
|
||||
name: 'testName',
|
||||
cohort: 'A',
|
||||
}
|
||||
|
||||
return cohorts.set(cohortTest).then(() => {
|
||||
return cohorts.getByName(cohortTest.name).then((cohort) => {
|
||||
expect(cohort).to.eq(cohortTest)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ require('../spec_helper')
|
||||
const path = require('path')
|
||||
const chokidar = require('chokidar')
|
||||
const pkg = require('@packages/root')
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
const { sinon } = require('../spec_helper')
|
||||
const user = require(`../../lib/user`)
|
||||
@@ -44,7 +45,7 @@ describe.skip('lib/project-base', () => {
|
||||
.then((obj = {}) => {
|
||||
({ projectId: this.projectId } = obj)
|
||||
|
||||
return config.setupFullConfigWithDefaults({ projectName: 'project', projectRoot: '/foo/bar' }, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults({ projectName: 'project', projectRoot: '/foo/bar' }, getCtx().file.getFilesByGlob)
|
||||
.then((config1) => {
|
||||
this.config = config1
|
||||
this.project = new ProjectBase({ projectRoot: this.todosPath, testingType: 'e2e' })
|
||||
|
||||
@@ -5,7 +5,7 @@ const os = require('os')
|
||||
const express = require('express')
|
||||
const Promise = require('bluebird')
|
||||
const { connect } = require('@packages/network')
|
||||
const config = require(`../../lib/config`)
|
||||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const fileServer = require(`../../lib/file_server`)
|
||||
@@ -22,7 +22,7 @@ describe('lib/server', () => {
|
||||
beforeEach(function () {
|
||||
this.server = new ServerE2E()
|
||||
|
||||
return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults({ projectRoot: '/foo/bar/', config: { supportFile: false } }, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
this.config = cfg
|
||||
})
|
||||
@@ -38,7 +38,7 @@ describe('lib/server', () => {
|
||||
})
|
||||
|
||||
// TODO: Figure out correct configuration to run these tests and/or which ones we need to keep.
|
||||
// The introducion of server-base/socket-base and the `ensureProp` function made unit testing
|
||||
// The introduction of server-base/socket-base and the `ensureProp` function made unit testing
|
||||
// the server difficult.
|
||||
describe.skip('lib/server', () => {
|
||||
beforeEach(function () {
|
||||
@@ -51,7 +51,7 @@ describe.skip('lib/server', () => {
|
||||
|
||||
sinon.stub(fileServer, 'create').returns(this.fileServer)
|
||||
|
||||
return config.setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }, getCtx().file.getFilesByGlob)
|
||||
return setupFullConfigWithDefaults({ projectRoot: '/foo/bar/' }, getCtx().file.getFilesByGlob)
|
||||
.then((cfg) => {
|
||||
this.config = cfg
|
||||
this.server = new ServerE2E()
|
||||
|
||||
@@ -3,21 +3,22 @@ require('../spec_helper')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const socketIo = require('@packages/socket/lib/browser')
|
||||
const httpsAgent = require('https-proxy-agent')
|
||||
|
||||
const errors = require(`../../lib/errors`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { Automation } = require(`../../lib/automation`)
|
||||
const exec = require(`../../lib/exec`)
|
||||
const preprocessor = require(`../../lib/plugins/preprocessor`)
|
||||
const { fs } = require(`../../lib/util/fs`)
|
||||
|
||||
const socketIo = require('@packages/socket/lib/browser')
|
||||
const Fixtures = require('@tooling/system-tests')
|
||||
const firefoxUtil = require(`../../lib/browsers/firefox-util`).default
|
||||
const { createRoutes } = require(`../../lib/routes`)
|
||||
const { getCtx } = require(`../../lib/makeDataContext`)
|
||||
|
||||
const errors = require('../../lib/errors')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { Automation } = require('../../lib/automation')
|
||||
const exec = require('../../lib/exec')
|
||||
const preprocessor = require('../../lib/plugins/preprocessor')
|
||||
const { fs } = require('../../lib/util/fs')
|
||||
const session = require('../../lib/session')
|
||||
|
||||
const firefoxUtil = require('../../lib/browsers/firefox-util').default
|
||||
const { createRoutes } = require('../../lib/routes')
|
||||
const { getCtx } = require('../../lib/makeDataContext')
|
||||
const { sinon } = require('../spec_helper')
|
||||
|
||||
let ctx
|
||||
@@ -33,6 +34,7 @@ describe('lib/socket', () => {
|
||||
sinon.stub(ctx.actions.project, 'initializeActiveProject')
|
||||
|
||||
Fixtures.scaffold()
|
||||
session.clearSessions(true)
|
||||
|
||||
this.todosPath = Fixtures.projectPath('todos')
|
||||
|
||||
@@ -453,7 +455,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(get:fixture)', () => {
|
||||
context('on(backend:request, get:fixture)', () => {
|
||||
it('returns the fixture object', function (done) {
|
||||
const cb = function (resp) {
|
||||
expect(resp.response).to.deep.eq([
|
||||
@@ -488,7 +490,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(http:request)', () => {
|
||||
context('on(backend:request, http:request)', () => {
|
||||
it('calls socket#onRequest', function (done) {
|
||||
sinon.stub(this.options, 'onRequest').resolves({ foo: 'bar' })
|
||||
|
||||
@@ -512,7 +514,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(exec)', () => {
|
||||
context('on(backend:request, exec)', () => {
|
||||
it('calls exec#run with project root and options', function (done) {
|
||||
const run = sinon.stub(exec, 'run').returns(Promise.resolve('Desktop Music Pictures'))
|
||||
|
||||
@@ -539,7 +541,7 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(firefox:force:gc)', () => {
|
||||
context('on(backend:request, firefox:force:gc)', () => {
|
||||
it('calls firefoxUtil#collectGarbage', function (done) {
|
||||
sinon.stub(firefoxUtil, 'collectGarbage').resolves()
|
||||
|
||||
@@ -598,6 +600,180 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, save:session)', () => {
|
||||
it('saves spec sessions', function (done) {
|
||||
const sessionData = {
|
||||
id: 'spec',
|
||||
cacheAcrossSpecs: false,
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'save:session', sessionData, () => {
|
||||
const state = session.getState()
|
||||
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {},
|
||||
specSessions: {
|
||||
'spec': sessionData,
|
||||
},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('saves global sessions', function (done) {
|
||||
const sessionData = {
|
||||
id: 'global',
|
||||
cacheAcrossSpecs: true,
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'save:session', sessionData, () => {
|
||||
const state = session.getState()
|
||||
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {
|
||||
'global': sessionData,
|
||||
},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error if session data has no id', function (done) {
|
||||
const sessionData = {}
|
||||
|
||||
this.client.emit('backend:request', 'save:session', sessionData, ({ error }) => {
|
||||
expect(error.message).to.eq('session data had no id')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, clear:sessions)', () => {
|
||||
it('clears spec sessions', function (done) {
|
||||
let state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
state.specSessions = {
|
||||
spec: { id: 'spec' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'clear:sessions', false, () => {
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {
|
||||
'global': { id: 'global' },
|
||||
},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('clears all sessions', function (done) {
|
||||
let state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
state.specSessions = {
|
||||
spec: { id: 'spec' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'clear:sessions', true, () => {
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, get:session)', () => {
|
||||
it('returns global session', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'get:session', 'global', ({ response, error }) => {
|
||||
expect(error).to.be.undefined
|
||||
expect(response).deep.eq({
|
||||
id: 'global',
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns spec session', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {}
|
||||
state.specSessions = {
|
||||
'spec': { id: 'spec' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'get:session', 'spec', ({ response, error }) => {
|
||||
expect(error).to.be.undefined
|
||||
expect(response).deep.eq({
|
||||
id: 'spec',
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns error when session does not exist', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {}
|
||||
state.specSessions = {}
|
||||
this.client.emit('backend:request', 'get:session', 1, ({ response, error }) => {
|
||||
expect(response).to.be.undefined
|
||||
expect(error.message).to.eq('session with id "1" not found')
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, reset:cached:test:state)', () => {
|
||||
it('clears spec sessions', function (done) {
|
||||
const state = session.getState()
|
||||
|
||||
state.globalSessions = {
|
||||
global: { id: 'global' },
|
||||
}
|
||||
|
||||
state.specSessions = {
|
||||
local: { id: 'local' },
|
||||
}
|
||||
|
||||
this.client.emit('backend:request', 'reset:cached:test:state', ({ error }) => {
|
||||
expect(error).to.be.undefined
|
||||
|
||||
expect(state).to.deep.eq({
|
||||
globalSessions: {
|
||||
'global': { id: 'global' },
|
||||
},
|
||||
specSessions: {},
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('unit', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface Cache {
|
||||
PROJECTS: string[]
|
||||
PROJECT_PREFERENCES: Record<string, Preferences>
|
||||
USER: CachedUser
|
||||
COHORTS: Record<string, Cohort>
|
||||
}
|
||||
|
||||
import type { AllowedState } from './preferences'
|
||||
@@ -13,3 +14,8 @@ export interface CachedUser {
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface Cohort {
|
||||
name: string
|
||||
cohort: string
|
||||
}
|
||||
|
||||
@@ -1,19 +1,46 @@
|
||||
export interface RunState {
|
||||
import type { ReporterRunState, StudioRecorderState } from './reporter'
|
||||
|
||||
interface MochaRunnerState {
|
||||
startTime?: number
|
||||
currentId?: number
|
||||
currentId?: number | null
|
||||
emissions?: Emissions
|
||||
tests?: unknown
|
||||
tests?: Record<string, Cypress.ObjectLike>
|
||||
passed?: number
|
||||
failed?: number
|
||||
pending?: number
|
||||
numLogs?: number
|
||||
}
|
||||
|
||||
export type RunState = MochaRunnerState & ReporterRunState & {
|
||||
studio?: StudioRecorderState
|
||||
isSpecsListOpen?: boolean
|
||||
}
|
||||
|
||||
export interface Emissions {
|
||||
started: Record<string, boolean>
|
||||
ended: Record<string, boolean>
|
||||
}
|
||||
|
||||
interface HtmlWebStorage {
|
||||
origin: string
|
||||
value: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ServerSessionData {
|
||||
id: string
|
||||
cacheAcrossSpecs: boolean
|
||||
cookies: Cypress.Cookie[] | null
|
||||
localStorage: Array<HtmlWebStorage> | null
|
||||
sessionStorage: Array<HtmlWebStorage> | null
|
||||
setup: string
|
||||
}
|
||||
|
||||
export type StoredSessions = Record<string, ServerSessionData>
|
||||
|
||||
export interface CachedTestState {
|
||||
activeSessions: StoredSessions
|
||||
}
|
||||
|
||||
export type Instrument = 'agent' | 'command' | 'route'
|
||||
|
||||
export type TestState = 'active' | 'failed' | 'pending' | 'passed' | 'processing'
|
||||
|
||||
@@ -28,6 +28,8 @@ export {
|
||||
RESOLVED_FROM,
|
||||
} from './config'
|
||||
|
||||
export * from './reporter'
|
||||
|
||||
export * from './server'
|
||||
|
||||
export * from './util'
|
||||
|
||||
@@ -25,10 +25,7 @@ export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
|
||||
'firstOpenedCypress',
|
||||
'showedStudioModal',
|
||||
'preferredOpener',
|
||||
'ctReporterWidth',
|
||||
'ctIsSpecsListOpen',
|
||||
'isSpecsListOpen',
|
||||
'ctSpecListWidth',
|
||||
'firstOpened',
|
||||
'lastOpened',
|
||||
'lastProjectId',
|
||||
@@ -61,9 +58,6 @@ export type AllowedState = Partial<{
|
||||
firstOpenedCypress: Maybe<number>
|
||||
showedStudioModal: Maybe<boolean>
|
||||
preferredOpener: Editor | undefined
|
||||
ctReporterWidth: Maybe<number>
|
||||
ctIsSpecsListOpen: Maybe<boolean>
|
||||
ctSpecListWidth: Maybe<number>
|
||||
lastProjectId: Maybe<string>
|
||||
firstOpened: Maybe<number>
|
||||
lastOpened: Maybe<number>
|
||||
|
||||
24
packages/types/src/reporter.ts
Normal file
24
packages/types/src/reporter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface StudioRecorderState {
|
||||
suiteId?: string
|
||||
testId?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface ReporterRunState {
|
||||
autoScrollingEnabled?: boolean
|
||||
scrollTop?: number
|
||||
}
|
||||
|
||||
export interface StatsStoreStartInfo {
|
||||
startTime: string
|
||||
numPassed?: number
|
||||
numFailed?: number
|
||||
numPending?: number
|
||||
}
|
||||
|
||||
export interface ReporterStartInfo extends StatsStoreStartInfo {
|
||||
isSpecsListOpen: boolean
|
||||
autoScrollingEnabled: boolean
|
||||
scrollTop: number
|
||||
studioActive: boolean
|
||||
}
|
||||
@@ -275,15 +275,15 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 3 found (spec.cy.js, stdout_specfile.cy.js, stdout_specfile_display_spec_with_a_re │
|
||||
│ ally_long_name_that_never_has_a_line_break_or_new_line.cy.js) │
|
||||
│ Searched: cypress/e2e/nested-1/nested-2/nested-3/* │
|
||||
│ Specs: 4 found (spec.cy.js, stdout_specfile.cy.js, stdout_specfile_display_spec_with_a_re │
|
||||
│ ally_long_name_that_never_has_a_line_break_or_new_line.cy.js, nested-4/spec.cy.js) │
|
||||
│ Searched: cypress/e2e/nested-1/nested-2/nested-3/**/* │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: spec.cy.js (1 of 3)
|
||||
Running: spec.cy.js (1 of 4)
|
||||
|
||||
|
||||
stdout_specfile_display_spec
|
||||
@@ -316,7 +316,7 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: stdout_specfile.cy.js (2 of 3)
|
||||
Running: stdout_specfile.cy.js (2 of 4)
|
||||
|
||||
|
||||
stdout_specfile_display_spec
|
||||
@@ -349,7 +349,7 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: stdout_specfile_display_spec_with_a_really_long_name_that_never_has_ (3 of 3)
|
||||
Running: stdout_specfile_display_spec_with_a_really_long_name_that_never_has_ (3 of 4)
|
||||
a_line_break_or_new_line.cy.js
|
||||
|
||||
|
||||
@@ -391,6 +391,39 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
ne.cy.js.mp4
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: nested-4/spec.cy.js (4 of 4)
|
||||
|
||||
|
||||
stdout_specfile_display_spec
|
||||
✓ passes
|
||||
|
||||
|
||||
1 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 1 │
|
||||
│ Passing: 1 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
│ Screenshots: 0 │
|
||||
│ Video: true │
|
||||
│ Duration: X seconds │
|
||||
│ Spec Ran: nested-4/spec.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
(Video)
|
||||
|
||||
- Started processing: Compressing to 32 CRF
|
||||
- Finished processing: /XXX/XXX/XXX/cypress/videos/nested-4/spec.cy.js.mp4 (X second)
|
||||
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Finished)
|
||||
@@ -405,8 +438,10 @@ exports['e2e stdout displays fullname of nested specfile 1'] = `
|
||||
│ ✔ stdout_specfile_display_spec_with_a XX:XX 1 1 - - - │
|
||||
│ _really_long_name_that_never_has_a_ │
|
||||
│ line_break_or_new_line.cy.js │
|
||||
├────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ✔ nested-4/spec.cy.js XX:XX 1 1 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 3 3 - - -
|
||||
✔ All specs passed! XX:XX 4 4 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user