Merge branch 'develop' into chore/develop-into-release-13

This commit is contained in:
Bill Glesias
2023-06-21 15:58:44 -04:00
139 changed files with 4926 additions and 6278 deletions

View File

@@ -14,12 +14,14 @@ _Released 08/1/2023 (PENDING)_
## 12.15.0
_Released 06/20/2023 (PENDING)_
_Released 06/20/2023_
**Features:**
- Added support for running Cypress tests with [Chrome's new `--headless=new` flag](https://developer.chrome.com/articles/new-headless/). Chrome versions 112 and above will now be run in the `headless` mode that matches the `headed` browser implementation. Addresses [#25972](https://github.com/cypress-io/cypress/issues/25972).
- Cypress can now test pages with targeted `Content-Security-Policy` and `Content-Security-Policy-Report-Only` header directives by specifying the allow list via the [`experimentalCspAllowList`](https://docs.cypress.io/guides/references/configuration#Experimental-Csp-Allow-List) configuration option. Addresses [#1030](https://github.com/cypress-io/cypress/issues/1030). Addressed in [#26483](https://github.com/cypress-io/cypress/pull/26483)
- The [`videoCompression`](https://docs.cypress.io/guides/references/configuration#Videos) configuration option now accepts both a boolean or a Constant Rate Factor (CRF) number between `1` and `51`. The `videoCompression` default value is still `32` CRF and when `videoCompression` is set to `true` the default of `32` CRF will be used. Addresses [#26658](https://github.com/cypress-io/cypress/issues/26658).
- The Cypress Cloud data shown on the [Specs](https://docs.cypress.io/guides/core-concepts/cypress-app#Specs) page and [Runs](https://docs.cypress.io/guides/core-concepts/cypress-app#Runs) page will now reflect Cloud Runs that match the current Git tree if Git is being used. Addresses [#26693](https://github.com/cypress-io/cypress/issues/26693).
**Bugfixes:**

View File

@@ -3,8 +3,8 @@ type EventEmitter2 = import("eventemitter2").EventEmitter2
interface CyEventEmitter extends Omit<EventEmitter2, 'waitFor'> {
proxyTo: (cy: Cypress.cy) => null
emitMap: (eventName: string, args: any[]) => Array<(...args: any[]) => any>
emitThen: (eventName: string, args: any[]) => Bluebird.BluebirdStatic
emitMap: (eventName: string, ...args: any[]) => Array<(...args: any[]) => any>
emitThen: (eventName: string, ...args: any[]) => Bluebird.BluebirdStatic
}
// Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/events.d.ts

View File

@@ -2672,6 +2672,8 @@ declare namespace Cypress {
force: boolean
}
type experimentalCspAllowedDirectives = 'default-src' | 'child-src' | 'frame-src' | 'script-src' | 'script-src-elem' | 'form-action'
type scrollBehaviorOptions = false | 'center' | 'top' | 'bottom' | 'nearest'
/**
@@ -3041,6 +3043,19 @@ declare namespace Cypress {
* @default 'top'
*/
scrollBehavior: scrollBehaviorOptions
/**
* Indicates whether Cypress should allow CSP header directives from the application under test.
* - When this option is set to `false`, Cypress will strip the entire CSP header.
* - When this option is set to `true`, Cypress will only to strip directives that would interfere
* with or inhibit Cypress functionality.
* - When this option to an array of allowable directives (`[ 'default-src', ... ]`), the directives
* specified will remain in the response headers.
*
* Please see the documentation for more information.
* @see https://on.cypress.io/experiments#Experimental-CSP-Allow-List
* @default false
*/
experimentalCspAllowList: boolean | experimentalCspAllowedDirectives[],
/**
* Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode.
* @default false
@@ -3052,7 +3067,7 @@ declare namespace Cypress {
* Please see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity.
* This option has no impact on experimentalSourceRewriting and is only used with the
* non-experimental source rewriter.
* @see https://on.cypress.io/configuration#experimentalModifyObstructiveThirdPartyCode
* @see https://on.cypress.io/experiments#Configuration
*/
experimentalModifyObstructiveThirdPartyCode: boolean
/**
@@ -3062,6 +3077,7 @@ declare namespace Cypress {
* navigations, and will require the use of cy.origin(). This option takes an array of
* strings/string globs.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/domain
* @see https://on.cypress.io/experiments#Experimental-Skip-Domain-Injection
* @default null
*/
experimentalSkipDomainInjection: string[] | null

View File

@@ -1,6 +1,6 @@
{
"name": "cypress",
"version": "12.14.0",
"version": "12.15.0",
"description": "Cypress is a next generation front end testing tool built for the modern web",
"private": true,
"scripts": {

View File

@@ -1,4 +1,5 @@
cypress/videos/*
cypress/screenshots/*
cypress/downloads/*
components.d.ts
components.d.ts

View File

@@ -11,6 +11,7 @@ export default defineConfig({
reporterOptions: {
configFile: '../../mocha-reporter-config.json',
},
experimentalCspAllowList: false,
experimentalInteractiveRunEvents: true,
component: {
experimentalSingleTabRunMode: true,

View File

@@ -1,5 +1,7 @@
import type { OpenFileInIdeQuery } from '../../src/generated/graphql-test'
import RelevantRunsDataSource_RunsByCommitShas from '../fixtures/gql-RelevantRunsDataSource_RunsByCommitShas.json'
import DebugDataPassing from '../fixtures/debug-Passing/gql-Debug.json'
import DebugDataFailing from '../fixtures/debug-Failing/gql-Debug.json'
Cypress.on('window:before:load', (win) => {
win.__CYPRESS_GQL_NO_SOCKET__ = 'true'
@@ -16,45 +18,26 @@ describe('App - Debug Page', () => {
cy.startAppServer('component')
cy.loginUser()
cy.withCtx((ctx) => {
cy.withCtx((ctx, o) => {
ctx.git?.__setGitHashesForTesting(['commit1', 'commit2'])
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
})
})
it('all tests passed', () => {
cy.remoteGraphQLIntercept((obj, _testState, options) => {
if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') {
obj.result.data = options.RelevantRunsDataSource_RunsByCommitShas.data
}
if (obj.operationName === 'Debug_currentProject_cloudProject_cloudProjectBySlug') {
if (obj.result.data) {
obj.result.data.cloudProjectBySlug.runByNumber = options.DebugDataPassing.data.currentProject.cloudProject.runByNumber
}
}
return obj.result
}, { RelevantRunsDataSource_RunsByCommitShas })
})
it('all tests passed', () => {
// This mocks all the responses so we can get deterministic
// results to test the debug page.
cy.intercept('query-Debug', {
fixture: 'debug-Passing/gql-Debug.json',
})
cy.intercept('query-CloudViewerAndProject_RequiredData', {
fixture: 'debug-Passing/gql-CloudViewerAndProject_RequiredData.json',
})
cy.intercept('query-MainAppQuery', {
fixture: 'debug-Passing/gql-MainAppQuery.json',
})
cy.intercept('query-SideBarNavigationContainer', {
fixture: 'debug-Passing/gql-SideBarNavigationContainer',
})
cy.intercept('query-HeaderBar_HeaderBarQuery', {
fixture: 'debug-Passing/gql-HeaderBar_HeaderBarQuery',
})
cy.intercept('query-SpecsPageContainer', {
fixture: 'debug-Passing/gql-SpecsPageContainer',
})
}, { RelevantRunsDataSource_RunsByCommitShas, DebugDataPassing })
cy.visitApp()
@@ -84,32 +67,24 @@ describe('App - Debug Page', () => {
cy.findByTestId('debug-passed').contains('All your tests passed.')
cy.findByLabelText('Relevant run passed').should('be.visible').contains('0')
cy.findByTestId('run-failures').should('not.exist')
cy.get('[data-cy="debug-badge"]').should('be.visible').contains('0')
})
it('shows information about a failed spec', () => {
cy.intercept('query-Debug', {
fixture: 'debug-Failing/gql-Debug.json',
})
cy.remoteGraphQLIntercept((obj, _testState, options) => {
if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') {
obj.result.data = options.RelevantRunsDataSource_RunsByCommitShas.data
}
cy.intercept('query-CloudViewerAndProject_RequiredData', {
fixture: 'debug-Failing/gql-CloudViewerAndProject_RequiredData.json',
})
if (obj.operationName === 'Debug_currentProject_cloudProject_cloudProjectBySlug') {
if (obj.result.data) {
obj.result.data.cloudProjectBySlug.runByNumber = options.DebugDataFailing.data.currentProject.cloudProject.runByNumber
}
}
cy.intercept('query-MainAppQuery', {
fixture: 'debug-Failing/gql-MainAppQuery.json',
})
cy.intercept('query-SideBarNavigationContainer', {
fixture: 'debug-Failing/gql-SideBarNavigationContainer',
})
cy.intercept('query-HeaderBar_HeaderBarQuery', {
fixture: 'debug-Failing/gql-HeaderBar_HeaderBarQuery',
})
cy.intercept('query-SpecsPageContainer', {
fixture: 'debug-Failing/gql-SpecsPageContainer',
})
return obj.result
}, { RelevantRunsDataSource_RunsByCommitShas, DebugDataFailing })
cy.intercept('query-OpenFileInIDE', (req) => {
req.on('response', (res) => {
@@ -152,7 +127,7 @@ describe('App - Debug Page', () => {
})
cy.findByTestId('spec-contents').within(() => {
cy.contains('src/components/InfoPanel/InfoPanel.cy.ts')
cy.contains('src/NewComponent.spec.jsx')
cy.findByTestId('metaData-Results-spec-duration').contains('00:04')
cy.findByTestId('metaData-Results-operating-system').contains('Linux Ubuntu')
cy.findByTestId('metaData-Results-browser').contains('Electron 106')
@@ -161,12 +136,95 @@ describe('App - Debug Page', () => {
cy.findByTestId('test-row').contains('InfoPanel')
cy.findByTestId('test-row').contains('renders')
cy.findByTestId('run-failures').should('exist').should('have.attr', 'href', '#/specs/runner?file=src/components/InfoPanel/InfoPanel.cy.ts&mode=debug')
cy.findByTestId('run-failures').should('exist').should('have.attr', 'href', '#/specs/runner?file=src/NewComponent.spec.jsx&mode=debug')
cy.findByLabelText('Open in IDE').click()
cy.wait('@openFileInIDE')
cy.withCtx((ctx) => {
expect(ctx.actions.file.openFile).to.have.been.calledWith('src/components/InfoPanel/InfoPanel.cy.ts', 1, 1)
expect(ctx.actions.file.openFile).to.have.been.calledWith('src/NewComponent.spec.jsx', 1, 1)
})
})
it('shows running and updating build', () => {
cy.remoteGraphQLIntercept((obj, _testState, options) => {
if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') {
obj.result.data = options.RelevantRunsDataSource_RunsByCommitShas.data
}
const originalRun = options.DebugDataFailing.data.currentProject.cloudProject.runByNumber
if (options.testRun === undefined) {
options.testRun = JSON.parse(JSON.stringify(originalRun))
}
const run = options.testRun
run.totalInstanceCount = 5
if (run.completedInstanceCount === undefined) {
run.completedInstanceCount = 0
run.createdAt = (new Date()).toISOString()
}
if (run.totalInstanceCount === run.completedInstanceCount) {
run.status = 'FAILED'
} else {
run.status = 'RUNNING'
}
if (run.completedInstanceCount < 3) {
run.testsForReview = []
} else {
run.testsForReview = originalRun.testsForReview
}
run.totalFailed = run.testsForReview.length
run.totalPassed = run.completedInstanceCount - run.totalFailed
if (obj.operationName === 'Debug_currentProject_cloudProject_cloudProjectBySlug') {
if (obj.result.data) {
obj.result.data.cloudProjectBySlug.runByNumber = run
}
}
if (obj.operationName === 'RelevantRunSpecsDataSource_Specs' && obj.result.data) {
//NOTE Figure out how to manually trigger polling instead of adjusting polling intervals
obj.result.data.pollingIntervals = {
__typename: 'CloudPollingIntervals',
runByNumber: 1, //Increase polling interval for debugging test
}
if (run.totalInstanceCount === run.completedInstanceCount) {
obj.result.data.pollingIntervals.runByNumber = 100
} else {
run.completedInstanceCount = run.completedInstanceCount !== undefined ? ++run.completedInstanceCount : 0
}
obj.result.data.cloudNodesByIds = [
run,
]
}
return obj.result
}, { RelevantRunsDataSource_RunsByCommitShas, DebugDataFailing })
cy.visitApp()
cy.findByTestId('sidebar-link-debug-page').click()
cy.findByTestId('debug-container').should('be.visible')
cy.findByTestId('header-top').contains('chore: testing cypress')
cy.findByTestId('debug-testing-progress').as('progress')
cy.get('@progress').contains('Testing in progress...')
cy.get('[data-cy="debug-badge"]').contains('0').should('be.visible')
cy.get('@progress').contains('1 of 5 specs completed')
cy.get('@progress').contains('2 of 5 specs completed')
cy.get('@progress').contains('3 of 5 specs completed')
cy.get('[data-cy="debug-badge"]').contains('1').should('be.visible')
cy.findByTestId('spec-contents').within(() => {
cy.contains('src/NewComponent.spec.jsx')
})
})
})

View File

@@ -343,18 +343,6 @@ export const generateCtErrorTests = (server: 'Webpack' | 'Vite', configFile: str
})
})
it('cy.readFile', () => {
const verify = loadErrorSpec({
filePath: 'errors/readfile.cy.js',
failCount: 1,
}, configFile)
verify('existence failure', {
column: [8, 9],
message: 'failed because the file does not exist',
})
})
it('validation errors', () => {
const verify = loadErrorSpec({
filePath: 'errors/validation.cy.js',

View File

@@ -321,18 +321,6 @@ describe('errors ui', {
})
})
it('cy.readFile', () => {
const verify = loadErrorSpec({
filePath: 'errors/readfile.cy.js',
failCount: 1,
})
verify('existence failure', {
column: 8,
message: 'failed because the file does not exist',
})
})
it('validation errors', () => {
const verify = loadErrorSpec({
filePath: 'errors/validation.cy.js',

View File

@@ -2,10 +2,6 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
import type { SinonStub } from 'sinon'
function moveToRunsPage (): void {
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
})
cy.findByTestId('sidebar-link-runs-page').click()
cy.findByTestId('app-header-bar').findByText('Runs').should('be.visible')
cy.findByTestId('runs-container').should('be.visible')
@@ -38,15 +34,12 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests')
cy.startAppServer('component')
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
})
})
it('resolves the runs page', () => {
cy.loginUser()
cy.visitApp()
cy.get('[href="#/runs"]', { timeout: 1000 }).click()
moveToRunsPage()
cy.get('[data-cy="runs"]')
cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('be.visible')
})
@@ -322,7 +315,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.withCtx(async (ctx, o) => {
o.sinon.spy(ctx.cloud, 'executeRemoteGraphQL')
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
//o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
const config = await ctx.project.getConfig()
expect(config.projectId).to.not.equal('newProjectId')
@@ -648,98 +641,156 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
})
context('Runs - Runs List', () => {
beforeEach(() => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests')
cy.startAppServer('component')
})
it('displays a list of recorded runs if a run has been recorded', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[data-cy="runs"]')
})
it('displays each run with correct information', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[href^="http://dummy.cypress.io/runs/0"]').first().within(() => {
cy.findByText('fix: make gql work CANCELLED')
cy.get('[data-cy="run-card-icon-CANCELLED"]')
context('no Git data', () => {
beforeEach(() => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests')
cy.startAppServer('component')
})
cy.get('[href^="http://dummy.cypress.io/runs/1"]').first().within(() => {
cy.findByText('fix: make gql work ERRORED')
cy.get('[data-cy="run-card-icon-ERRORED"]')
it('displays a list of recorded runs if a run has been recorded', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[data-cy="runs"]')
})
cy.get('[href^="http://dummy.cypress.io/runs/2"]').first().within(() => {
cy.findByText('fix: make gql work FAILED')
cy.get('[data-cy="run-card-icon-FAILED"]')
it('displays each run with correct information', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[href^="http://dummy.cypress.io/runs/0"]').first().within(() => {
cy.findByText('fix: make gql work CANCELLED')
cy.get('[data-cy="run-card-icon-CANCELLED"]')
})
cy.get('[href^="http://dummy.cypress.io/runs/1"]').first().within(() => {
cy.findByText('fix: make gql work ERRORED')
cy.get('[data-cy="run-card-icon-ERRORED"]')
})
cy.get('[href^="http://dummy.cypress.io/runs/2"]').first().within(() => {
cy.findByText('fix: make gql work FAILED')
cy.get('[data-cy="run-card-icon-FAILED"]')
})
cy.get('[href^="http://dummy.cypress.io/runs/0"]').first().as('firstRun')
cy.get('@firstRun').within(() => {
cy.get('[data-cy="run-card-author"]').contains('John Appleseed')
cy.get('[data-cy="run-card-avatar"]')
cy.get('[data-cy="run-card-branch"]').contains('main')
cy.get('[data-cy="run-card-created-at"]').contains('an hour ago')
cy.get('[data-cy="run-card-duration"]').contains('01:00')
cy.contains('span', 'skipped')
cy.get('span').contains('pending')
cy.get('span').contains('passed')
cy.get('span').contains('failed')
})
})
cy.get('[href^="http://dummy.cypress.io/runs/0"]').first().as('firstRun')
it('opens the run page if a run is clicked', () => {
cy.loginUser()
cy.visitApp()
cy.get('@firstRun').within(() => {
cy.get('[data-cy="run-card-author"]').contains('John Appleseed')
cy.get('[data-cy="run-card-avatar"]')
cy.get('[data-cy="run-card-branch"]').contains('main')
cy.get('[data-cy="run-card-created-at"]').contains('an hour ago')
cy.get('[data-cy="run-card-duration"]').contains('01:00')
moveToRunsPage()
cy.get('[data-cy^="runCard-"]').first().click()
cy.contains('span', 'skipped')
cy.get('span').contains('pending')
cy.get('span').contains('passed')
cy.get('span').contains('failed')
cy.withCtx((ctx) => {
expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.contain('http://dummy.cypress.io/runs/0')
})
})
})
it('opens the run page if a run is clicked', () => {
cy.loginUser()
cy.visitApp()
it('shows connection failed error if no cloudProject', () => {
let cloudData: any
moveToRunsPage()
cy.get('[data-cy^="runCard-"]').first().click()
cy.loginUser()
cy.remoteGraphQLIntercept((obj) => {
if (obj.operationName?.includes('cloudProject_cloudProjectBySlug')) {
cloudData = obj.result
obj.result = {}
cy.withCtx((ctx) => {
expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.contain('http://dummy.cypress.io/runs/0')
})
})
it('shows connection failed error if no cloudProject', () => {
let cloudData: any
cy.loginUser()
cy.remoteGraphQLIntercept((obj) => {
if (obj.operationName?.includes('cloudProject_cloudProjectBySlug')) {
cloudData = obj.result
obj.result = {}
return obj.result
}
return obj.result
}
})
return obj.result
cy.visitApp()
moveToRunsPage()
cy.contains('h2', 'Cannot connect to Cypress Cloud')
// cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435
cy.remoteGraphQLIntercept((obj) => {
if (obj.operationName?.includes('cloudProject_cloudProjectBySlug')) {
return cloudData
}
return obj.result
})
cy.contains('button', 'Try again').click().should('not.exist')
})
})
context('has Git data', () => {
beforeEach(() => {
cy.scaffoldProject('component-tests')
.then((projectPath) => {
cy.task('initGitRepoForTestProject', projectPath)
cy.openProject('component-tests')
cy.startAppServer('component')
})
})
cy.visitApp()
moveToRunsPage()
cy.contains('h2', 'Cannot connect to Cypress Cloud')
// cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435
cy.remoteGraphQLIntercept((obj) => {
if (obj.operationName?.includes('cloudProject_cloudProjectBySlug')) {
return cloudData
}
return obj.result
it('displays a list of recorded runs if a run has been recorded', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[data-cy="runs"]')
})
cy.contains('button', 'Try again').click().should('not.exist')
it('displays each run with correct information', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[href^="http://dummy.cypress.io/runs/0"]').first().within(() => {
cy.findByText('fix: using Git data CANCELLED')
cy.get('[data-cy="run-card-icon-CANCELLED"]')
})
cy.get('[href^="http://dummy.cypress.io/runs/0"]').first().as('firstRun')
cy.get('@firstRun').within(() => {
cy.get('[data-cy="run-card-author"]').contains('John Appleseed')
cy.get('[data-cy="run-card-avatar"]')
cy.get('[data-cy="run-card-branch"]').contains('main')
cy.get('[data-cy="run-card-created-at"]').contains('an hour ago')
cy.get('[data-cy="run-card-duration"]').contains('01:00')
cy.contains('span', 'skipped')
cy.get('span').contains('pending')
cy.get('span').contains('passed')
cy.get('span').contains('failed')
})
})
it('opens the run page if a run is clicked', () => {
cy.loginUser()
cy.visitApp()
moveToRunsPage()
cy.get('[data-cy^="runCard-"]').first().click()
cy.withCtx((ctx) => {
expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.contain('http://dummy.cypress.io/runs/0')
})
})
})
})
@@ -765,10 +816,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
})
it('should remove the alert warning if the app reconnects to the internet', () => {
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
})
cy.loginUser()
cy.visitApp()
cy.wait(1000)
@@ -783,7 +830,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.goOnline()
cy.get('[data-cy=warning-alert]').should('not.exist')
cy.contains('You have no internet connection').should('not.exist')
})
it('shows correct message on create org modal', () => {
@@ -861,39 +908,41 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
const RUNNING_COUNT = 3
describe('refetching', () => {
let obj: {toCall?: Function} = {}
beforeEach(() => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests')
cy.startAppServer('component')
cy.loginUser()
cy.remoteGraphQLIntercept((obj) => {
if (obj.result.data?.cloudProjectBySlug?.runs?.nodes.length) {
obj.result.data.cloudProjectBySlug.runs.nodes.map((run) => {
run.status = 'RUNNING'
})
if (obj.operationName === 'Runs_currentProject_cloudProject_cloudProjectBySlug') {
if (obj.result.data?.cloudProjectBySlug?.runs?.nodes.length) {
obj.result.data.cloudProjectBySlug.runs.nodes.map((run) => {
run.status = 'RUNNING'
})
obj.result.data.cloudProjectBySlug.runs.nodes = obj.result.data.cloudProjectBySlug.runs.nodes.slice(0, 3)
obj.result.data.cloudProjectBySlug.runs.nodes = obj.result.data.cloudProjectBySlug.runs.nodes.slice(0, 3)
}
}
if (obj.operationName === 'RelevantRunSpecsDataSource_Specs') {
if (obj.result.data?.cloudNodesByIds) {
obj.result.data?.cloudNodesByIds.map((node) => {
node.status = 'RUNNING'
})
}
if (obj.result.data) {
obj.result.data.pollingIntervals = {
__typename: 'CloudPollingIntervals',
runByNumber: 0.1,
}
}
}
return obj.result
})
cy.visitApp('/runs', {
onBeforeLoad (win) {
const setTimeout = win.setTimeout
// @ts-expect-error
win.setTimeout = function (fn: () => void, time: number) {
if (fn.name === 'fetchNewerRuns') {
obj.toCall = fn
} else {
setTimeout(fn, time)
}
}
},
})
cy.visitApp('/runs')
})
// https://github.com/cypress-io/cypress/issues/24575
@@ -921,54 +970,13 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
})
function completeNext (passed) {
cy.wrap(obj).invoke('toCall').then(() => {
cy.get('[data-cy="run-card-icon-PASSED"]').should('have.length', passed).should('be.visible')
if (passed < RUNNING_COUNT) {
completeNext(passed + 1)
}
})
cy.get('[data-cy="run-card-icon-PASSED"]').should('have.length', passed).should('be.visible')
if (passed < RUNNING_COUNT) {
completeNext(passed + 1)
}
}
completeNext(1)
})
// TODO: unskip https://github.com/cypress-io/cypress/issues/24575
it.skip('should fetch newer runs and maintain them when navigating', () => {
cy.get('[data-cy="run-card-icon-RUNNING"]').should('have.length', RUNNING_COUNT).should('be.visible')
cy.remoteGraphQLIntercept(async (obj) => {
await new Promise((resolve) => setTimeout(resolve, 100))
if (obj.result.data?.cloudNodesByIds) {
obj.result.data?.cloudNodesByIds.map((node) => {
node.status = 'PASSED'
node.totalPassed = 100
})
}
return obj.result
})
cy.get('[data-cy="run-card-icon-RUNNING"]').should('have.length', 3).should('be.visible')
cy.wrap(obj).invoke('toCall')
cy.get('[data-cy="run-card-icon-PASSED"]')
.should('have.length', 3)
.should('be.visible')
.first().within(() => {
cy.get('[data-cy="runResults-passed-count"]').should('contain', 100)
})
cy.get('[data-cy="run-card-icon-RUNNING"]').should('have.length', 2).should('be.visible')
// If we navigate away & back, we should see the same runs
cy.findByTestId('sidebar-link-settings-page').click()
cy.remoteGraphQLIntercept((obj) => obj.result)
moveToRunsPage()
cy.get('[data-cy="run-card-icon-PASSED"]').should('have.length', 3).should('be.visible')
cy.get('[data-cy="run-card-icon-RUNNING"]').should('have.length', 2).should('be.visible')
})
})
})

View File

@@ -10,6 +10,8 @@ describe('App: Spec List - Flaky Indicator', () => {
o.sinon.stub(ctx.project, 'projectId').resolves('abc123')
// Must have an active Git branch in order to fetch flaky data (see @include($hasBranch) restriction)
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
ctx.git?.__setGitHashesForTesting(['commit1', 'commit2'])
})
cy.remoteGraphQLIntercept(async (obj) => {
@@ -20,13 +22,10 @@ describe('App: Spec List - Flaky Indicator', () => {
__typename: 'CloudProjectSpec',
id: `id${obj.variables.specPath}`,
retrievedAt: new Date().toISOString(),
averageDuration: null,
specRuns: {
__typename: 'CloudSpecRunConnection',
nodes: [],
},
isConsideredFlaky: true,
flakyStatus: {
averageDurationForRunIds: null,
specRunsForRunIds: [],
isConsideredFlakyForRunIds: true,
flakyStatusForRunIds: {
__typename: 'CloudProjectSpecFlakyStatus',
severity: 'LOW',
flakyRuns: 2,
@@ -38,6 +37,41 @@ describe('App: Spec List - Flaky Indicator', () => {
}
}
if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') {
obj.result.data = {
'cloudProjectBySlug': {
'__typename': 'CloudProject',
'id': 'Q2xvdWRQcm9qZWN0OnZncXJ3cA==',
'runsByCommitShas': [
{
'id': 'Q2xvdWRSdW46TUdWZXhvQkRPNg==',
'runNumber': 136,
'status': 'FAILED',
'commitInfo': {
'sha': 'commit2',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
},
{
'id': 'Q2xvdWRSdW46ckdXb2wzbzJHVg==',
'runNumber': 134,
'status': 'PASSED',
'commitInfo': {
'sha': '37fa5bfb9e774d00a03fe8f0d439f06ec70f533d',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
},
],
},
'pollingIntervals': {
'runsByCommitShas': 30,
'__typename': 'CloudPollingIntervals',
},
}
}
return obj.result
})
@@ -50,13 +84,10 @@ describe('App: Spec List - Flaky Indicator', () => {
__typename: 'CloudProjectSpec',
id: `id${obj.variables.specPath}`,
retrievedAt: new Date().toISOString(),
averageDuration: null,
specRuns: {
__typename: 'CloudSpecRunConnection',
nodes: [],
},
isConsideredFlaky: true,
flakyStatus: {
averageDurationForRunIds: null,
specRunsForRunIds: [],
isConsideredFlakyForRunIds: true,
flakyStatusForRunIds: {
__typename: 'CloudProjectSpecFlakyStatus',
severity: 'LOW',
flakyRuns: 2,
@@ -71,21 +102,10 @@ describe('App: Spec List - Flaky Indicator', () => {
__typename: 'CloudProjectSpec',
id: `id${obj.variables.specPath}`,
retrievedAt: new Date().toISOString(),
averageDuration: null,
specRuns: {
__typename: 'CloudSpecRunConnection',
nodes: [],
},
isConsideredFlaky: false,
flakyStatus: null,
}
}
if (obj.field === 'cloudLatestRunUpdateSpecData') {
return {
__typename: 'CloudLatestRunUpdateSpecData',
mostRecentUpdate: new Date('2022-06-10').toISOString(),
pollingInterval: 60,
averageDurationForRunIds: null,
specRunsForRunIds: [],
isConsideredFlakyForRunIds: false,
flakyStatusForRunIds: null,
}
}

View File

@@ -48,7 +48,7 @@ function specShouldShow (specFileName: string, runDotsClasses: string[], latestR
const latestStatusSpinning = latestRunStatus === 'RUNNING'
type dotIndex = Parameters<typeof dotSelector>[1];
const indexes: dotIndex[] = [0, 1, 2]
const indexes: Exclude<dotIndex, 'latest'>[] = [0, 1, 2]
indexes.forEach((i) => {
return cy.get(dotSelector(specFileName, i)).should('have.class', `icon-light-${runDotsClasses.length > i ? runDotsClasses[i] : 'gray-300'}`)
@@ -66,6 +66,45 @@ function specShouldShow (specFileName: string, runDotsClasses: string[], latestR
}
function simulateRunData () {
cy.remoteGraphQLIntercept(async (obj) => {
if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') {
obj.result.data = {
'cloudProjectBySlug': {
'__typename': 'CloudProject',
'id': 'Q2xvdWRQcm9qZWN0OnZncXJ3cA==',
'runsByCommitShas': [
{
'id': 'Q2xvdWRSdW46TUdWZXhvQkRPNg==',
'runNumber': 136,
'status': 'FAILED',
'commitInfo': {
'sha': 'commit2',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
},
{
'id': 'Q2xvdWRSdW46ckdXb2wzbzJHVg==',
'runNumber': 134,
'status': 'PASSED',
'commitInfo': {
'sha': '37fa5bfb9e774d00a03fe8f0d439f06ec70f533d',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
},
],
},
'pollingIntervals': {
'runsByCommitShas': 30,
'__typename': 'CloudPollingIntervals',
},
}
}
return obj.result
})
cy.remoteGraphQLInterceptBatched(async (obj) => {
if (obj.field !== 'cloudSpecByPath') {
return obj.result
@@ -132,11 +171,8 @@ function simulateRunData () {
__typename: 'CloudProjectSpec',
retrievedAt: new Date().toISOString(),
id: `id${obj.variables.specPath}`,
specRuns: {
__typename: 'CloudSpecRunConnection',
nodes: runs,
},
averageDuration,
specRunsForRunIds: runs,
averageDurationForRunIds: averageDuration,
}
})
}
@@ -159,6 +195,7 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch')
ctx.git?.__setGitHashesForTesting(['commit1', 'commit2'])
})
})
@@ -410,15 +447,55 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
cy.remoteGraphQLIntercept(async (obj, testState) => {
const pollingCounter = testState.pollingCounter ?? 0
if (obj.result.data && 'cloudLatestRunUpdateSpecData' in obj.result.data) {
const mostRecentUpdate = pollingCounter > 1 ? new Date().toISOString() : new Date('2022-06-10').toISOString()
// initial polling interval is set to every second to avoid long wait times
const pollingInterval = pollingCounter > 1 ? 30 : 1
if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') {
obj.result.data = {
'cloudProjectBySlug': {
'__typename': 'CloudProject',
'id': 'Q2xvdWRQcm9qZWN0OnZncXJ3cA==',
'runsByCommitShas': [
{
'id': 'Q2xvdWRSdW46TUdWZXhvQkRPNg==',
'runNumber': 136,
'status': 'PASSED',
'commitInfo': {
'sha': 'commit2',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
},
{
'id': 'Q2xvdWRSdW46ckdXb2wzbzJHVg==',
'runNumber': 134,
'status': 'FAILED',
'commitInfo': {
'sha': '37fa5bfb9e774d00a03fe8f0d439f06ec70f533d',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
},
],
},
'pollingIntervals': {
'runsByCommitShas': 1,
'__typename': 'CloudPollingIntervals',
},
}
obj.result.data.cloudLatestRunUpdateSpecData = {
__typename: 'CloudLatestRunUpdateSpecData',
mostRecentUpdate,
pollingInterval,
if (pollingCounter > 2) {
obj.result.data.cloudProjectBySlug.runsByCommitShas.shift({
'id': 'Q2xvdWRSdW46TUdWZXhvQkRPNg==',
'runNumber': 138,
'status': 'FAILED',
'commitInfo': {
'sha': 'commit2',
'__typename': 'CloudRunCommitInfo',
},
'__typename': 'CloudRun',
})
}
if (pollingCounter > 5) {
obj.result.data.pollingIntervals.runsByCommitShas = 100
}
testState.pollingCounter = pollingCounter + 1
@@ -488,11 +565,8 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
__typename: 'CloudProjectSpec',
retrievedAt: new Date().toISOString(),
id: `id${obj.variables.specPath}`,
specRuns: {
__typename: 'CloudSpecRunConnection',
nodes: runs,
},
averageDuration,
specRunsForRunIds: runs,
averageDurationForRunIds: averageDuration,
}
})
@@ -519,125 +593,6 @@ describe('App/Cloud Integration - Latest runs and Average duration', { viewportW
cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:13')
})
})
context('polling indicates no new data', () => {
beforeEach(() => {
cy.loginUser()
cy.remoteGraphQLIntercept(async (obj, testState) => {
const pollingCounter = testState.pollingCounter ?? 0
if (obj.result.data && 'cloudLatestRunUpdateSpecData' in obj.result.data) {
const mostRecentUpdate = new Date('2022-06-10').toISOString()
// initial polling interval is set to every second to avoid long wait times
const pollingInterval = pollingCounter > 1 ? 30 : 1
obj.result.data.cloudLatestRunUpdateSpecData = {
__typename: 'CloudLatestRunUpdateSpecData',
mostRecentUpdate,
pollingInterval,
}
testState.pollingCounter = pollingCounter + 1
}
return obj.result
})
cy.remoteGraphQLInterceptBatched(async (obj, testState) => {
if (obj.field !== 'cloudSpecByPath') {
return obj.result
}
const fakeRuns = (statuses: string[], idPrefix: string) => {
return statuses.map((s, idx) => {
return {
__typename: 'CloudSpecRun',
id: `SpecRun_${idPrefix}_${idx}`,
status: s,
createdAt: new Date('2022-05-08T03:17:00').toISOString(),
completedAt: new Date('2022-05-08T05:17:00').toISOString(),
basename: idPrefix.substring(idPrefix.lastIndexOf('/') + 1, idPrefix.indexOf('.')),
path: idPrefix,
extension: idPrefix.substring(idPrefix.indexOf('.')),
runNumber: 432,
groupCount: 2,
specDuration: {
min: 143003, // 2:23
max: 159120, // 3:40
__typename: 'SpecDataAggregate',
},
testsFailed: {
min: 1,
max: 2,
__typename: 'SpecDataAggregate',
},
testsPassed: {
min: 22,
max: 23,
__typename: 'SpecDataAggregate',
},
testsSkipped: {
min: null,
max: null,
__typename: 'SpecDataAggregate',
},
testsPending: {
min: 1,
max: 2,
__typename: 'SpecDataAggregate',
},
url: 'https://google.com',
}
})
}
const pollingCounter = testState.pollingCounter ?? 0
// simulate network latency to allow for caching to register
await new Promise((r) => setTimeout(r, 20))
const statuses = pollingCounter < 2 ? ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : ['FAILED', 'PASSED', 'FAILED', 'CANCELLED', 'ERRORED']
const runs = fakeRuns(statuses, obj.variables.specPath)
const averageDuration = pollingCounter < 2 ? 12000 : 13000
return {
__typename: 'CloudProjectSpec',
retrievedAt: new Date().toISOString(),
id: `id${obj.variables.specPath}`,
specRuns: {
__typename: 'CloudSpecRunConnection',
nodes: runs,
},
averageDuration,
}
})
cy.visitApp()
cy.findByTestId('sidebar-link-specs-page').click()
})
it('shows the same data after polling', () => {
specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter')
cy.get('.v-popper__popper--shown').should('exist')
validateTooltip('Passed')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave')
cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12')
cy.wait(1200)
// new results should be shown
specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter')
cy.get('.v-popper__popper--shown').should('exist')
validateTooltip('Passed')
cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave')
cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12')
})
})
})
describe('App/Cloud Integration - Latest runs and Average duration', { viewportWidth: 1200 }, () => {

View File

@@ -1,462 +0,0 @@
{
"data": {
"cloudViewer": {
"id": "Q2xvdWRVc2VyOjcxYTM3NmVhLTdlMGUtNDBhOS1hMTAzLWMwM2NmNTMyMmQyZg==",
"fullName": "Lachlan Miller",
"email": "lachlan.miller.1990@outlook.com",
"firstOrganization": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"__typename": "CloudUser"
},
"cachedUser": {
"id": "Q2FjaGVkVXNlcjpsYWNobGFuLm1pbGxlci4xOTkwQG91dGxvb2suY29t",
"fullName": "Lachlan Miller",
"email": "lachlan.miller.1990@outlook.com",
"__typename": "CachedUser"
},
"authState": {
"name": null,
"__typename": "AuthState"
},
"currentProject": {
"id": "debug-test-project-id",
"config": [
{
"value": 5,
"from": "default",
"field": "animationDistanceThreshold"
},
{
"value": "arm64",
"from": "default",
"field": "arch"
},
{
"value": null,
"from": "default",
"field": "baseUrl"
},
{
"value": null,
"from": "default",
"field": "blockHosts"
},
{
"value": true,
"from": "default",
"field": "chromeWebSecurity"
},
{
"value": [],
"from": "default",
"field": "clientCertificates"
},
{
"value": 4000,
"from": "default",
"field": "defaultCommandTimeout"
},
{
"value": "cypress/downloads",
"from": "default",
"field": "downloadsFolder"
},
{
"value": {
"INTERNAL_CLOUD_ENV": "production",
"INTERNAL_GRAPHQL_PORT": 4444,
"INTERNAL_EVENT_COLLECTOR_ENV": "staging",
"CONFIG_ENV": "production"
},
"field": "env",
"from": "env"
},
{
"value": 60000,
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalFetchPolyfill"
},
{
"value": false,
"from": "default",
"field": "experimentalInteractiveRunEvents"
},
{
"value": false,
"from": "default",
"field": "experimentalRunAllSpecs"
},
{
"value": false,
"from": "default",
"field": "experimentalMemoryManagement"
},
{
"value": false,
"from": "default",
"field": "experimentalModifyObstructiveThirdPartyCode"
},
{
"value": null,
"from": "default",
"field": "experimentalSkipDomainInjection"
},
{
"value": false,
"from": "default",
"field": "experimentalOriginDependencies"
},
{
"value": false,
"from": "default",
"field": "experimentalSourceRewriting"
},
{
"value": true,
"from": "config",
"field": "experimentalSingleTabRunMode"
},
{
"value": false,
"from": "default",
"field": "experimentalStudio"
},
{
"value": false,
"from": "default",
"field": "experimentalWebKitSupport"
},
{
"value": "",
"from": "default",
"field": "fileServerFolder"
},
{
"value": "cypress/fixtures",
"from": "default",
"field": "fixturesFolder"
},
{
"value": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"from": "default",
"field": "excludeSpecPattern"
},
{
"value": false,
"from": "default",
"field": "includeShadowDom"
},
{
"value": 0,
"from": "default",
"field": "keystrokeDelay"
},
{
"value": true,
"from": "default",
"field": "modifyObstructiveCode"
},
{
"from": "default",
"field": "nodeVersion"
},
{
"value": 50,
"from": "default",
"field": "numTestsKeptInMemory"
},
{
"value": "darwin",
"from": "default",
"field": "platform"
},
{
"value": 60000,
"from": "default",
"field": "pageLoadTimeout"
},
{
"value": null,
"from": "default",
"field": "port"
},
{
"value": "vgqrwp",
"from": "config",
"field": "projectId"
},
{
"value": 20,
"from": "default",
"field": "redirectionLimit"
},
{
"value": "spec",
"from": "default",
"field": "reporter"
},
{
"value": null,
"from": "default",
"field": "reporterOptions"
},
{
"value": 5000,
"from": "default",
"field": "requestTimeout"
},
{
"value": null,
"from": "default",
"field": "resolvedNodePath"
},
{
"value": null,
"from": "default",
"field": "resolvedNodeVersion"
},
{
"value": 30000,
"from": "default",
"field": "responseTimeout"
},
{
"value": {
"runMode": 0,
"openMode": 0
},
"from": "default",
"field": "retries"
},
{
"value": true,
"from": "default",
"field": "screenshotOnRunFailure"
},
{
"value": "cypress/screenshots",
"from": "default",
"field": "screenshotsFolder"
},
{
"value": 250,
"from": "default",
"field": "slowTestThreshold"
},
{
"value": "top",
"from": "default",
"field": "scrollBehavior"
},
{
"value": "cypress/support/component.{js,jsx,ts,tsx}",
"from": "default",
"field": "supportFile"
},
{
"value": false,
"from": "default",
"field": "supportFolder"
},
{
"value": 60000,
"from": "default",
"field": "taskTimeout"
},
{
"value": true,
"from": "default",
"field": "testIsolation"
},
{
"value": true,
"from": "default",
"field": "trashAssetsBeforeRuns"
},
{
"value": null,
"from": "default",
"field": "userAgent"
},
{
"value": false,
"from": "default",
"field": "video"
},
{
"value": false,
"from": "default",
"field": "videoCompression"
},
{
"value": "cypress/videos",
"from": "default",
"field": "videosFolder"
},
{
"value": 500,
"from": "default",
"field": "viewportHeight"
},
{
"value": 500,
"from": "default",
"field": "viewportWidth"
},
{
"value": true,
"from": "default",
"field": "waitForAnimations"
},
{
"value": true,
"from": "default",
"field": "watchForFileChanges"
},
{
"value": "**/*.cy.{js,jsx,ts,tsx}",
"from": "default",
"field": "specPattern"
},
{
"value": [
{
"name": "chrome",
"family": "chromium",
"channel": "stable",
"displayName": "Chrome",
"version": "109.0.5414.119",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"minSupportedVersion": 64,
"majorVersion": "109"
},
{
"name": "firefox",
"family": "firefox",
"channel": "stable",
"displayName": "Firefox",
"version": "107.0.1",
"path": "/Applications/Firefox.app/Contents/MacOS/firefox",
"minSupportedVersion": 86,
"majorVersion": "107"
},
{
"name": "electron",
"channel": "stable",
"family": "chromium",
"displayName": "Electron",
"version": "106.0.5249.51",
"path": "",
"majorVersion": 106
}
],
"from": "runtime",
"field": "browsers"
},
{
"value": null,
"from": "default",
"field": "hosts"
},
{
"value": true,
"from": "default",
"field": "isInteractive"
}
],
"isFullConfigReady": true,
"hasNonExampleSpec": true,
"savedState": {
"firstOpened": 1674605493218,
"lastOpened": 1675053721981,
"lastProjectId": "vgqrwp",
"specFilter": ""
},
"cloudProject": {
"__typename": "CloudProject",
"id": "cloud-project-test-id",
"runs": {
"nodes": [
{
"id": "Q2xvdWRSdW46TUdWZXhvQkRPNg==",
"status": "FAILED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/136",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46Nk9kdm93eG45cQ==",
"status": "FAILED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/135",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46ckdXb2wzbzJHVg==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/134",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46WUc0eDVZMFZHUA==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/133",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46VjkxMHJvRGpHcg==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/132",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46ZU9qeWtCUFlMcQ==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/131",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46ajl4bjhYV05PbA==",
"status": "FAILED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/130",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46a0wzRVBlNTBHdw==",
"status": "FAILED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/129",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46Vk9KNnhkVmVPYg==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/128",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46SzlFTlEyb05MYg==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/127",
"__typename": "CloudRun"
}
],
"__typename": "CloudRunConnection"
}
},
"__typename": "CurrentProject"
}
}
}

View File

@@ -78,10 +78,10 @@
"specs": [
{
"id": "Q2xvdWRTcGVjUnVuOmY0YzE3OGIxLWRlZjktNGI1NC1hOTU1LWQ3MGU0NDhjMTg5MTpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
"path": "src/components/InfoPanel/InfoPanel.cy.ts",
"basename": "InfoPanel.cy.ts",
"extension": ".cy.ts",
"shortPath": "src/components/InfoPanel/InfoPanel.cy.ts",
"path": "src/NewComponent.spec.jsx",
"basename": "NewComponent.spec.jsx",
"extension": ".spec.jsx",
"shortPath": "src/NewComponent.spec.jsx",
"groupIds": [
"Q2xvdWRSdW5Hcm91cDo2Njg2MTI4NjpsaW51eC1FbGVjdHJvbi0xMDYtYjAyZTk4NDJiNQ=="
],

View File

@@ -1,656 +0,0 @@
{
"data": {
"currentProject": {
"id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
"title": "frontend",
"config": [
{
"value": 5,
"from": "default",
"field": "animationDistanceThreshold"
},
{
"value": "arm64",
"from": "default",
"field": "arch"
},
{
"value": null,
"from": "default",
"field": "baseUrl"
},
{
"value": null,
"from": "default",
"field": "blockHosts"
},
{
"value": true,
"from": "default",
"field": "chromeWebSecurity"
},
{
"value": [],
"from": "default",
"field": "clientCertificates"
},
{
"value": 4000,
"from": "default",
"field": "defaultCommandTimeout"
},
{
"value": "cypress/downloads",
"from": "default",
"field": "downloadsFolder"
},
{
"value": {
"INTERNAL_CLOUD_ENV": "production",
"INTERNAL_GRAPHQL_PORT": 4444,
"INTERNAL_EVENT_COLLECTOR_ENV": "staging",
"CONFIG_ENV": "production"
},
"field": "env",
"from": "env"
},
{
"value": 60000,
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalFetchPolyfill"
},
{
"value": false,
"from": "default",
"field": "experimentalInteractiveRunEvents"
},
{
"value": false,
"from": "default",
"field": "experimentalRunAllSpecs"
},
{
"value": false,
"from": "default",
"field": "experimentalMemoryManagement"
},
{
"value": false,
"from": "default",
"field": "experimentalModifyObstructiveThirdPartyCode"
},
{
"value": null,
"from": "default",
"field": "experimentalSkipDomainInjection"
},
{
"value": false,
"from": "default",
"field": "experimentalOriginDependencies"
},
{
"value": false,
"from": "default",
"field": "experimentalSourceRewriting"
},
{
"value": true,
"from": "config",
"field": "experimentalSingleTabRunMode"
},
{
"value": false,
"from": "default",
"field": "experimentalStudio"
},
{
"value": false,
"from": "default",
"field": "experimentalWebKitSupport"
},
{
"value": "",
"from": "default",
"field": "fileServerFolder"
},
{
"value": "cypress/fixtures",
"from": "default",
"field": "fixturesFolder"
},
{
"value": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"from": "default",
"field": "excludeSpecPattern"
},
{
"value": false,
"from": "default",
"field": "includeShadowDom"
},
{
"value": 0,
"from": "default",
"field": "keystrokeDelay"
},
{
"value": true,
"from": "default",
"field": "modifyObstructiveCode"
},
{
"from": "default",
"field": "nodeVersion"
},
{
"value": 50,
"from": "default",
"field": "numTestsKeptInMemory"
},
{
"value": "darwin",
"from": "default",
"field": "platform"
},
{
"value": 60000,
"from": "default",
"field": "pageLoadTimeout"
},
{
"value": null,
"from": "default",
"field": "port"
},
{
"value": "vgqrwp",
"from": "config",
"field": "projectId"
},
{
"value": 20,
"from": "default",
"field": "redirectionLimit"
},
{
"value": "spec",
"from": "default",
"field": "reporter"
},
{
"value": null,
"from": "default",
"field": "reporterOptions"
},
{
"value": 5000,
"from": "default",
"field": "requestTimeout"
},
{
"value": null,
"from": "default",
"field": "resolvedNodePath"
},
{
"value": null,
"from": "default",
"field": "resolvedNodeVersion"
},
{
"value": 30000,
"from": "default",
"field": "responseTimeout"
},
{
"value": {
"runMode": 0,
"openMode": 0
},
"from": "default",
"field": "retries"
},
{
"value": true,
"from": "default",
"field": "screenshotOnRunFailure"
},
{
"value": "cypress/screenshots",
"from": "default",
"field": "screenshotsFolder"
},
{
"value": 250,
"from": "default",
"field": "slowTestThreshold"
},
{
"value": "top",
"from": "default",
"field": "scrollBehavior"
},
{
"value": "cypress/support/component.{js,jsx,ts,tsx}",
"from": "default",
"field": "supportFile"
},
{
"value": false,
"from": "default",
"field": "supportFolder"
},
{
"value": 60000,
"from": "default",
"field": "taskTimeout"
},
{
"value": true,
"from": "default",
"field": "testIsolation"
},
{
"value": true,
"from": "default",
"field": "trashAssetsBeforeRuns"
},
{
"value": null,
"from": "default",
"field": "userAgent"
},
{
"value": false,
"from": "default",
"field": "video"
},
{
"value": false,
"from": "default",
"field": "videoCompression"
},
{
"value": "cypress/videos",
"from": "default",
"field": "videosFolder"
},
{
"value": 500,
"from": "default",
"field": "viewportHeight"
},
{
"value": 500,
"from": "default",
"field": "viewportWidth"
},
{
"value": true,
"from": "default",
"field": "waitForAnimations"
},
{
"value": true,
"from": "default",
"field": "watchForFileChanges"
},
{
"value": "**/*.cy.{js,jsx,ts,tsx}",
"from": "default",
"field": "specPattern"
},
{
"value": [
{
"name": "chrome",
"family": "chromium",
"channel": "stable",
"displayName": "Chrome",
"version": "109.0.5414.119",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"minSupportedVersion": 64,
"majorVersion": "109"
},
{
"name": "firefox",
"family": "firefox",
"channel": "stable",
"displayName": "Firefox",
"version": "107.0.1",
"path": "/Applications/Firefox.app/Contents/MacOS/firefox",
"minSupportedVersion": 86,
"majorVersion": "107"
},
{
"name": "electron",
"channel": "stable",
"family": "chromium",
"displayName": "Electron",
"version": "106.0.5249.51",
"path": "",
"majorVersion": 106
}
],
"from": "runtime",
"field": "browsers"
},
{
"value": null,
"from": "default",
"field": "hosts"
},
{
"value": true,
"from": "default",
"field": "isInteractive"
}
],
"savedState": {
"firstOpened": 1674605493218,
"lastOpened": 1675053721981,
"lastProjectId": "vgqrwp",
"specFilter": ""
},
"currentTestingType": "component",
"branch": "main",
"packageManager": "yarn",
"activeBrowser": {
"id": "QnJvd3NlcjpjaHJvbWUtY2hyb21pdW0tc3RhYmxl",
"displayName": "Chrome",
"majorVersion": "109",
"__typename": "Browser"
},
"browsers": [
{
"id": "QnJvd3NlcjpjaHJvbWUtY2hyb21pdW0tc3RhYmxl",
"isSelected": true,
"displayName": "Chrome",
"version": "109.0.5414.119",
"majorVersion": "109",
"isVersionSupported": true,
"warning": null,
"disabled": null,
"__typename": "Browser"
},
{
"id": "QnJvd3NlcjpmaXJlZm94LWZpcmVmb3gtc3RhYmxl",
"isSelected": false,
"displayName": "Firefox",
"version": "107.0.1",
"majorVersion": "107",
"isVersionSupported": true,
"warning": null,
"disabled": null,
"__typename": "Browser"
},
{
"id": "QnJvd3NlcjplbGVjdHJvbi1jaHJvbWl1bS1zdGFibGU=",
"isSelected": false,
"displayName": "Electron",
"version": "106.0.5249.51",
"majorVersion": "106",
"isVersionSupported": true,
"warning": null,
"disabled": null,
"__typename": "Browser"
}
],
"projectId": "vgqrwp",
"cloudProject": {
"__typename": "CloudProject",
"id": "Q2xvdWRQcm9qZWN0OnZncXJ3cA=="
},
"__typename": "CurrentProject"
},
"isGlobalMode": true,
"versions": {
"current": {
"id": "12.4.0",
"version": "12.4.0",
"released": "2023-01-24T18:40:53.125Z",
"__typename": "Version"
},
"latest": {
"id": "12.4.1",
"version": "12.4.1",
"released": "2023-01-27T15:00:32.366Z",
"__typename": "Version"
},
"__typename": "VersionData"
},
"cloudViewer": {
"id": "Q2xvdWRVc2VyOjcxYTM3NmVhLTdlMGUtNDBhOS1hMTAzLWMwM2NmNTMyMmQyZg==",
"cloudOrganizationsUrl": "https://cloud.cypress.io/organizations",
"organizations": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"name": "Org 2",
"projects": {
"nodes": [],
"__typename": "CloudProjectConnection"
},
"__typename": "CloudOrganization"
},
{
"id": "Q2xvdWRPcmdhbml6YXRpb246MDIxZmVhNjctZDYwOC00YWIyLWFmMTctM2Y4YTJhMjNkMDE5",
"name": "Lachlan's Personal Projects",
"projects": {
"nodes": [
{
"id": "Q2xvdWRQcm9qZWN0OnZncXJ3cA==",
"slug": "vgqrwp",
"name": "Rhythm Game",
"__typename": "CloudProject"
}
],
"__typename": "CloudProjectConnection"
},
"__typename": "CloudOrganization"
},
{
"id": "Q2xvdWRPcmdhbml6YXRpb246ODllYmMwOTktNzhjMS00YjIzLWIwYzMtNjAzMGY0MjAxNDBj",
"name": "Lachlan Miller",
"projects": {
"nodes": [
{
"id": "Q2xvdWRQcm9qZWN0Om9mODhoNQ==",
"slug": "of88h5",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Onp5N2dzZQ==",
"slug": "zy7gse",
"name": "express",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmZ1aDkzOQ==",
"slug": "fuh939",
"name": "bannerjs",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjVicHF0MQ==",
"slug": "5bpqt1",
"name": "baretest88",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjJ5dm1odQ==",
"slug": "2yvmhu",
"name": "baretest414141",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Ojk4dzhveQ==",
"slug": "98w8oy",
"name": "desktop-gui-testing",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmJqdWJjYQ==",
"slug": "bjubca",
"name": "baretest58",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmQ4ZjM5bQ==",
"slug": "d8f39m",
"name": "baretest00",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmR3am5vMg==",
"slug": "dwjno2",
"name": "baretest66",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmZ3ZHZ1Mw==",
"slug": "fwdvu3",
"name": "31baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnVxNHhyYg==",
"slug": "uq4xrb",
"name": "baretest33331",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Ong5Y3BzOQ==",
"slug": "x9cps9",
"name": "555baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmZ6bW53Yw==",
"slug": "fzmnwc",
"name": "baretestdd",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnU5Y3d2Zg==",
"slug": "u9cwvf",
"name": "baretest-41",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Om9rZDQ3OA==",
"slug": "okd478",
"name": "baretest-1231",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjkxNTZiMw==",
"slug": "9156b3",
"name": "baretest555",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmlvbmNhbg==",
"slug": "ioncan",
"name": "baretest-asdf",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnpuYm9qOQ==",
"slug": "znboj9",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmljczdteA==",
"slug": "ics7mx",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnN1cjRidw==",
"slug": "sur4bw",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjF1b2c1eA==",
"slug": "1uog5x",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Om52MXJ0OA==",
"slug": "nv1rt8",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmlnM2Nzaw==",
"slug": "ig3csk",
"name": "baretest-1",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjhlbWU2MQ==",
"slug": "8eme61",
"name": "rhythm-frontendddd",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Ojk4anA1Ng==",
"slug": "98jp56",
"name": "rhythm-frontend",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjNlNWJwYg==",
"slug": "3e5bpb",
"name": "Lachlan Miller Testing",
"__typename": "CloudProject"
}
],
"__typename": "CloudProjectConnection"
},
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"email": "lachlan.miller.1990@outlook.com",
"fullName": "Lachlan Miller",
"firstOrganization": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"__typename": "CloudUser"
},
"authState": {
"browserOpened": false,
"name": null,
"message": null,
"__typename": "AuthState"
},
"cachedUser": {
"id": "Q2FjaGVkVXNlcjpsYWNobGFuLm1pbGxlci4xOTkwQG91dGxvb2suY29t",
"fullName": "Lachlan Miller",
"email": "lachlan.miller.1990@outlook.com",
"__typename": "CachedUser"
}
}
}

View File

@@ -1,11 +0,0 @@
{
"data": {
"baseError": null,
"currentProject": {
"id": "debug-test-project-id",
"isLoadingConfigFile": false,
"isLoadingNodeEvents": false,
"__typename": "CurrentProject"
}
}
}

View File

@@ -1,35 +0,0 @@
{
"data": {
"localSettings": {
"preferences": {
"isSideNavigationOpen": true,
"isSpecsListOpen": false,
"autoScrollingEnabled": true,
"reporterWidth": 787,
"specListWidth": null,
"__typename": "LocalSettingsPreferences"
},
"__typename": "LocalSettings"
},
"currentProject": {
"id": "debug-test-project-id",
"cloudProject": {
"__typename": "CloudProject",
"id": "cloud-project-test-id",
"runByNumber": {
"id": "Q2xvdWRSdW46TUdWZXhvQkRPNg==",
"status": "FAILED",
"totalFailed": 1,
"__typename": "CloudRun"
}
},
"isCTConfigured": true,
"isE2EConfigured": true,
"currentTestingType": "component",
"title": "frontend",
"branch": "main",
"__typename": "CurrentProject"
},
"invokedFromCli": true
}
}

View File

@@ -1,809 +0,0 @@
{
"data": {
"currentProject": {
"id": "debug-test-project-id",
"projectRoot": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend",
"currentTestingType": "component",
"cloudProject": {
"__typename": "CloudProject",
"id": "cloud-project-test-id"
},
"specs": [
{
"id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9EaWZmaWN1bHR5SXRlbS5jeS50cw==",
"name": "src/components/DifficultyItem.cy.ts",
"specType": "component",
"absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/DifficultyItem.cy.ts",
"baseName": "DifficultyItem.cy.ts",
"fileName": "DifficultyItem",
"specFileExtension": ".cy.ts",
"fileExtension": ".ts",
"relative": "src/components/DifficultyItem.cy.ts",
"gitInfo": {
"lastModifiedTimestamp": "2022-07-21 19:00:38 +1000",
"lastModifiedHumanReadable": "6 months ago",
"author": "Lachlan Miller",
"statusType": "unmodified",
"shortHash": "a33f7f4",
"subject": "feat: cover (#7)",
"__typename": "GitInfo"
},
"cloudSpec": {
"id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaWRtZHhjbmR3SWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5RWFXWm1hV04xYkhSNVNYUmxiUzVqZVM1MGN5Sjk=",
"fetchingStatus": "FETCHED",
"data": {
"__typename": "CloudProjectSpec",
"id": "Q2xvdWRQcm9qZWN0U3BlYzp2Z3Fyd3A6YzNKakwyTnZiWEJ2Ym1WdWRITXZSR2xtWm1samRXeDBlVWwwWlcwdVkza3VkSE09",
"retrievedAt": "2023-01-30T04:42:05.607Z",
"averageDuration": 200,
"isConsideredFlaky": false,
"flakyStatus": {
"__typename": "CloudFeatureNotEnabled"
},
"specRuns": {
"nodes": [
{
"id": "Q2xvdWRTcGVjUnVuOmY0YzE3OGIxLWRlZjktNGI1NC1hOTU1LWQ3MGU0NDhjMTg5MTpaalU0TnpJeFltSXROek13T1Mxa05XWXlMV05pT1dNdE5UVTRZemRsTVdKak9HUTE=",
"runNumber": 136,
"basename": "DifficultyItem.cy.ts",
"path": "src/components/DifficultyItem.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-30T01:44:09.040Z",
"groupCount": 1,
"specDuration": {
"min": 107,
"max": 107,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/136/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22418f4eed-fcaf-4305-9624-d93ceed654a4%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyItem.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
},
{
"id": "Q2xvdWRTcGVjUnVuOjMzMjBlMjI0LWFmODktNGEyOS04OWM2LTRkZGUxNWFhZDYwMDpaalU0TnpJeFltSXROek13T1Mxa05XWXlMV05pT1dNdE5UVTRZemRsTVdKak9HUTE=",
"runNumber": 134,
"basename": "DifficultyItem.cy.ts",
"path": "src/components/DifficultyItem.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-29T07:08:42.978Z",
"groupCount": 1,
"specDuration": {
"min": 191,
"max": 191,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/134/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22d77709c2-aeb6-4ee3-9ae6-eaa452b56c2a%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyItem.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
},
{
"id": "Q2xvdWRTcGVjUnVuOmIxYWFlZTNlLWY2N2UtNDYxYS05MDM1LTk2ODBlYzY2YmJmYTpaalU0TnpJeFltSXROek13T1Mxa05XWXlMV05pT1dNdE5UVTRZemRsTVdKak9HUTE=",
"runNumber": 133,
"basename": "DifficultyItem.cy.ts",
"path": "src/components/DifficultyItem.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-26T07:23:21.660Z",
"groupCount": 1,
"specDuration": {
"min": 285,
"max": 285,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/133/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22753121da-5f8c-4ba6-91ae-2a16c3a52440%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyItem.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
},
{
"id": "Q2xvdWRTcGVjUnVuOjJlZWQ5NjY0LWQxNTMtNDEzYS04YmQzLWM2NjA5ZWRkOWIzNzpaalU0TnpJeFltSXROek13T1Mxa05XWXlMV05pT1dNdE5UVTRZemRsTVdKak9HUTE=",
"runNumber": 132,
"basename": "DifficultyItem.cy.ts",
"path": "src/components/DifficultyItem.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-26T05:25:07.357Z",
"groupCount": 1,
"specDuration": {
"min": 181,
"max": 181,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/132/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22be24810d-940c-4dc0-b9e8-a3d65eee64f5%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyItem.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
}
],
"__typename": "CloudSpecRunConnection"
}
},
"__typename": "RemoteFetchableCloudProjectSpecResult"
},
"__typename": "Spec"
},
{
"id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9JbmZvUGFuZWwvSW5mb1BhbmVsLmN5LnRz",
"name": "src/components/InfoPanel/InfoPanel.cy.ts",
"specType": "component",
"absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/InfoPanel/InfoPanel.cy.ts",
"baseName": "InfoPanel.cy.ts",
"fileName": "InfoPanel",
"specFileExtension": ".cy.ts",
"fileExtension": ".ts",
"relative": "src/components/InfoPanel/InfoPanel.cy.ts",
"gitInfo": {
"lastModifiedTimestamp": "2023-01-30 11:01:22 +1000",
"lastModifiedHumanReadable": "4 hours ago",
"author": "Lachlan Miller",
"statusType": "unmodified",
"shortHash": "commit1",
"subject": "chore: testing cypress",
"__typename": "GitInfo"
},
"cloudSpec": {
"id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaWRtZHhjbmR3SWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5SmJtWnZVR0Z1Wld3dlNXNW1iMUJoYm1Wc0xtTjVMblJ6SW4wPQ==",
"fetchingStatus": "FETCHED",
"data": {
"__typename": "CloudProjectSpec",
"id": "Q2xvdWRQcm9qZWN0U3BlYzp2Z3Fyd3A6YzNKakwyTnZiWEJ2Ym1WdWRITXZTVzVtYjFCaGJtVnNMMGx1Wm05UVlXNWxiQzVqZVM1MGN3PT0=",
"retrievedAt": "2023-01-30T04:42:05.608Z",
"averageDuration": 1440.3,
"isConsideredFlaky": false,
"flakyStatus": {
"__typename": "CloudFeatureNotEnabled"
},
"specRuns": {
"nodes": [
{
"id": "Q2xvdWRTcGVjUnVuOmY0YzE3OGIxLWRlZjktNGI1NC1hOTU1LWQ3MGU0NDhjMTg5MTpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
"runNumber": 136,
"basename": "InfoPanel.cy.ts",
"path": "src/components/InfoPanel/InfoPanel.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-30T01:44:09.040Z",
"groupCount": 1,
"specDuration": {
"min": 4509,
"max": 4509,
"__typename": "SpecDataAggregate"
},
"status": "FAILED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/136/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%229728b4a7-b420-403f-92e2-e07ea8506efc%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FInfoPanel%2FInfoPanel.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
},
{
"id": "Q2xvdWRTcGVjUnVuOjMzMjBlMjI0LWFmODktNGEyOS04OWM2LTRkZGUxNWFhZDYwMDpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
"runNumber": 134,
"basename": "InfoPanel.cy.ts",
"path": "src/components/InfoPanel/InfoPanel.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-29T07:08:42.978Z",
"groupCount": 1,
"specDuration": {
"min": 83,
"max": 83,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/134/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%2222a9f323-7052-46ec-ab0e-fa923cf3d705%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FInfoPanel%2FInfoPanel.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
},
{
"id": "Q2xvdWRTcGVjUnVuOmIxYWFlZTNlLWY2N2UtNDYxYS05MDM1LTk2ODBlYzY2YmJmYTpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
"runNumber": 133,
"basename": "InfoPanel.cy.ts",
"path": "src/components/InfoPanel/InfoPanel.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-26T07:23:21.660Z",
"groupCount": 1,
"specDuration": {
"min": 68,
"max": 68,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/133/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22d8cd0724-591b-4f77-ad75-7209d5c8902e%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FInfoPanel%2FInfoPanel.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
},
{
"id": "Q2xvdWRTcGVjUnVuOjJlZWQ5NjY0LWQxNTMtNDEzYS04YmQzLWM2NjA5ZWRkOWIzNzpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
"runNumber": 132,
"basename": "InfoPanel.cy.ts",
"path": "src/components/InfoPanel/InfoPanel.cy.ts",
"extension": ".cy.ts",
"testsFailed": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsPassed": {
"min": 1,
"max": 1,
"__typename": "SpecDataAggregate"
},
"testsPending": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"testsSkipped": {
"min": 0,
"max": 0,
"__typename": "SpecDataAggregate"
},
"createdAt": "2023-01-26T05:25:07.357Z",
"groupCount": 1,
"specDuration": {
"min": 93,
"max": 93,
"__typename": "SpecDataAggregate"
},
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/vgqrwp/runs/132/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22a895b0f2-aef4-4d8b-aa5b-4b3fba8abccc%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FInfoPanel%2FInfoPanel.cy.ts%22%7D%5D",
"__typename": "CloudSpecRun"
}
],
"__typename": "CloudSpecRunConnection"
}
},
"__typename": "RemoteFetchableCloudProjectSpecResult"
},
"__typename": "Spec"
}
],
"config": [
{
"value": 5,
"from": "default",
"field": "animationDistanceThreshold"
},
{
"value": "arm64",
"from": "default",
"field": "arch"
},
{
"value": null,
"from": "default",
"field": "baseUrl"
},
{
"value": null,
"from": "default",
"field": "blockHosts"
},
{
"value": true,
"from": "default",
"field": "chromeWebSecurity"
},
{
"value": [],
"from": "default",
"field": "clientCertificates"
},
{
"value": 4000,
"from": "default",
"field": "defaultCommandTimeout"
},
{
"value": "cypress/downloads",
"from": "default",
"field": "downloadsFolder"
},
{
"value": {
"INTERNAL_CLOUD_ENV": "production",
"INTERNAL_GRAPHQL_PORT": 4444,
"INTERNAL_EVENT_COLLECTOR_ENV": "staging",
"CONFIG_ENV": "production"
},
"field": "env",
"from": "env"
},
{
"value": 60000,
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalFetchPolyfill"
},
{
"value": false,
"from": "default",
"field": "experimentalInteractiveRunEvents"
},
{
"value": false,
"from": "default",
"field": "experimentalRunAllSpecs"
},
{
"value": false,
"from": "default",
"field": "experimentalMemoryManagement"
},
{
"value": false,
"from": "default",
"field": "experimentalModifyObstructiveThirdPartyCode"
},
{
"value": null,
"from": "default",
"field": "experimentalSkipDomainInjection"
},
{
"value": false,
"from": "default",
"field": "experimentalOriginDependencies"
},
{
"value": false,
"from": "default",
"field": "experimentalSourceRewriting"
},
{
"value": true,
"from": "config",
"field": "experimentalSingleTabRunMode"
},
{
"value": false,
"from": "default",
"field": "experimentalStudio"
},
{
"value": false,
"from": "default",
"field": "experimentalWebKitSupport"
},
{
"value": "",
"from": "default",
"field": "fileServerFolder"
},
{
"value": "cypress/fixtures",
"from": "default",
"field": "fixturesFolder"
},
{
"value": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"from": "default",
"field": "excludeSpecPattern"
},
{
"value": false,
"from": "default",
"field": "includeShadowDom"
},
{
"value": 0,
"from": "default",
"field": "keystrokeDelay"
},
{
"value": true,
"from": "default",
"field": "modifyObstructiveCode"
},
{
"from": "default",
"field": "nodeVersion"
},
{
"value": 50,
"from": "default",
"field": "numTestsKeptInMemory"
},
{
"value": "darwin",
"from": "default",
"field": "platform"
},
{
"value": 60000,
"from": "default",
"field": "pageLoadTimeout"
},
{
"value": null,
"from": "default",
"field": "port"
},
{
"value": "vgqrwp",
"from": "config",
"field": "projectId"
},
{
"value": 20,
"from": "default",
"field": "redirectionLimit"
},
{
"value": "spec",
"from": "default",
"field": "reporter"
},
{
"value": null,
"from": "default",
"field": "reporterOptions"
},
{
"value": 5000,
"from": "default",
"field": "requestTimeout"
},
{
"value": null,
"from": "default",
"field": "resolvedNodePath"
},
{
"value": null,
"from": "default",
"field": "resolvedNodeVersion"
},
{
"value": 30000,
"from": "default",
"field": "responseTimeout"
},
{
"value": {
"runMode": 0,
"openMode": 0
},
"from": "default",
"field": "retries"
},
{
"value": true,
"from": "default",
"field": "screenshotOnRunFailure"
},
{
"value": "cypress/screenshots",
"from": "default",
"field": "screenshotsFolder"
},
{
"value": 250,
"from": "default",
"field": "slowTestThreshold"
},
{
"value": "top",
"from": "default",
"field": "scrollBehavior"
},
{
"value": "cypress/support/component.{js,jsx,ts,tsx}",
"from": "default",
"field": "supportFile"
},
{
"value": false,
"from": "default",
"field": "supportFolder"
},
{
"value": 60000,
"from": "default",
"field": "taskTimeout"
},
{
"value": true,
"from": "default",
"field": "testIsolation"
},
{
"value": true,
"from": "default",
"field": "trashAssetsBeforeRuns"
},
{
"value": null,
"from": "default",
"field": "userAgent"
},
{
"value": false,
"from": "default",
"field": "video"
},
{
"value": false,
"from": "default",
"field": "videoCompression"
},
{
"value": "cypress/videos",
"from": "default",
"field": "videosFolder"
},
{
"value": 500,
"from": "default",
"field": "viewportHeight"
},
{
"value": 500,
"from": "default",
"field": "viewportWidth"
},
{
"value": true,
"from": "default",
"field": "waitForAnimations"
},
{
"value": true,
"from": "default",
"field": "watchForFileChanges"
},
{
"value": "**/*.cy.{js,jsx,ts,tsx}",
"from": "default",
"field": "specPattern"
},
{
"value": [
{
"name": "chrome",
"family": "chromium",
"channel": "stable",
"displayName": "Chrome",
"version": "109.0.5414.119",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"minSupportedVersion": 64,
"majorVersion": "109"
},
{
"name": "firefox",
"family": "firefox",
"channel": "stable",
"displayName": "Firefox",
"version": "107.0.1",
"path": "/Applications/Firefox.app/Contents/MacOS/firefox",
"minSupportedVersion": 86,
"majorVersion": "107"
},
{
"name": "electron",
"channel": "stable",
"family": "chromium",
"displayName": "Electron",
"version": "106.0.5249.51",
"path": "",
"majorVersion": 106
}
],
"from": "runtime",
"field": "browsers"
},
{
"value": null,
"from": "default",
"field": "hosts"
},
{
"value": true,
"from": "default",
"field": "isInteractive"
}
],
"savedState": {
"firstOpened": 1674605493218,
"lastOpened": 1675053721981,
"lastProjectId": "vgqrwp",
"specFilter": ""
},
"configFile": "cypress.config.ts",
"configFileAbsolutePath": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/cypress.config.ts",
"projectId": "vgqrwp",
"branch": "main",
"codeGenGlobs": {
"id": "Q29kZUdlbkdsb2JzOioudnVl",
"component": "*.vue",
"__typename": "CodeGenGlobs"
},
"fileExtensionToUse": "ts",
"defaultSpecFileName": "cypress/component/ComponentName.cy.tsx",
"codeGenFramework": "vue",
"isDefaultSpecPattern": true,
"__typename": "CurrentProject"
},
"cloudViewer": {
"id": "Q2xvdWRVc2VyOjcxYTM3NmVhLTdlMGUtNDBhOS1hMTAzLWMwM2NmNTMyMmQyZg==",
"firstOrganization": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"__typename": "CloudUser"
},
"cachedUser": {
"id": "Q2FjaGVkVXNlcjpsYWNobGFuLm1pbGxlci4xOTkwQG91dGxvb2suY29t",
"__typename": "CachedUser"
},
"localSettings": {
"availableEditors": [
{
"id": "computer",
"name": "Finder",
"binary": "computer",
"__typename": "Editor"
},
{
"id": "code",
"name": "Visual Studio Code",
"binary": "code",
"__typename": "Editor"
},
{
"id": "vim",
"name": "Vim",
"binary": "vim",
"__typename": "Editor"
}
],
"preferences": {
"preferredEditorBinary": null,
"__typename": "LocalSettingsPreferences"
},
"__typename": "LocalSettings"
}
}
}

View File

@@ -1,10 +0,0 @@
{
"data": {
"currentProject": {
"id": "debug-test-project-id",
"branch": "main",
"projectId": "vgqrwp",
"__typename": "CurrentProject"
}
}
}

View File

@@ -1,419 +0,0 @@
{
"data": {
"cloudViewer": {
"id": "Q2xvdWRVc2VyOjcxYTM3NmVhLTdlMGUtNDBhOS1hMTAzLWMwM2NmNTMyMmQyZg==",
"fullName": "Lachlan Miller",
"email": "lachlan.miller.1990@outlook.com",
"firstOrganization": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"__typename": "CloudUser"
},
"cachedUser": {
"id": "Q2FjaGVkVXNlcjpsYWNobGFuLm1pbGxlci4xOTkwQG91dGxvb2suY29t",
"fullName": "Lachlan Miller",
"email": "lachlan.miller.1990@outlook.com",
"__typename": "CachedUser"
},
"authState": {
"name": null,
"__typename": "AuthState"
},
"currentProject": {
"id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
"config": [
{
"value": 5,
"from": "default",
"field": "animationDistanceThreshold"
},
{
"value": "arm64",
"from": "default",
"field": "arch"
},
{
"value": null,
"from": "default",
"field": "baseUrl"
},
{
"value": null,
"from": "default",
"field": "blockHosts"
},
{
"value": true,
"from": "default",
"field": "chromeWebSecurity"
},
{
"value": [],
"from": "default",
"field": "clientCertificates"
},
{
"value": 4000,
"from": "default",
"field": "defaultCommandTimeout"
},
{
"value": "cypress/downloads",
"from": "default",
"field": "downloadsFolder"
},
{
"value": {
"INTERNAL_CLOUD_ENV": "production",
"INTERNAL_GRAPHQL_PORT": 4444,
"INTERNAL_EVENT_COLLECTOR_ENV": "staging",
"CONFIG_ENV": "production"
},
"field": "env",
"from": "env"
},
{
"value": 60000,
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalFetchPolyfill"
},
{
"value": false,
"from": "default",
"field": "experimentalInteractiveRunEvents"
},
{
"value": false,
"from": "default",
"field": "experimentalRunAllSpecs"
},
{
"value": false,
"from": "default",
"field": "experimentalMemoryManagement"
},
{
"value": false,
"from": "default",
"field": "experimentalModifyObstructiveThirdPartyCode"
},
{
"value": null,
"from": "default",
"field": "experimentalSkipDomainInjection"
},
{
"value": false,
"from": "default",
"field": "experimentalOriginDependencies"
},
{
"value": false,
"from": "default",
"field": "experimentalSourceRewriting"
},
{
"value": true,
"from": "config",
"field": "experimentalSingleTabRunMode"
},
{
"value": false,
"from": "default",
"field": "experimentalStudio"
},
{
"value": false,
"from": "default",
"field": "experimentalWebKitSupport"
},
{
"value": "",
"from": "default",
"field": "fileServerFolder"
},
{
"value": "cypress/fixtures",
"from": "default",
"field": "fixturesFolder"
},
{
"value": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"from": "default",
"field": "excludeSpecPattern"
},
{
"value": false,
"from": "default",
"field": "includeShadowDom"
},
{
"value": 0,
"from": "default",
"field": "keystrokeDelay"
},
{
"value": true,
"from": "default",
"field": "modifyObstructiveCode"
},
{
"from": "default",
"field": "nodeVersion"
},
{
"value": 50,
"from": "default",
"field": "numTestsKeptInMemory"
},
{
"value": "darwin",
"from": "default",
"field": "platform"
},
{
"value": 60000,
"from": "default",
"field": "pageLoadTimeout"
},
{
"value": null,
"from": "default",
"field": "port"
},
{
"value": "7p5uce",
"from": "config",
"field": "projectId"
},
{
"value": 20,
"from": "default",
"field": "redirectionLimit"
},
{
"value": "spec",
"from": "default",
"field": "reporter"
},
{
"value": null,
"from": "default",
"field": "reporterOptions"
},
{
"value": 5000,
"from": "default",
"field": "requestTimeout"
},
{
"value": null,
"from": "default",
"field": "resolvedNodePath"
},
{
"value": null,
"from": "default",
"field": "resolvedNodeVersion"
},
{
"value": 30000,
"from": "default",
"field": "responseTimeout"
},
{
"value": {
"runMode": 0,
"openMode": 0
},
"from": "default",
"field": "retries"
},
{
"value": true,
"from": "default",
"field": "screenshotOnRunFailure"
},
{
"value": "cypress/screenshots",
"from": "default",
"field": "screenshotsFolder"
},
{
"value": 250,
"from": "default",
"field": "slowTestThreshold"
},
{
"value": "top",
"from": "default",
"field": "scrollBehavior"
},
{
"value": "cypress/support/component.{js,jsx,ts,tsx}",
"from": "default",
"field": "supportFile"
},
{
"value": false,
"from": "default",
"field": "supportFolder"
},
{
"value": 60000,
"from": "default",
"field": "taskTimeout"
},
{
"value": true,
"from": "default",
"field": "testIsolation"
},
{
"value": true,
"from": "default",
"field": "trashAssetsBeforeRuns"
},
{
"value": null,
"from": "default",
"field": "userAgent"
},
{
"value": false,
"from": "default",
"field": "video"
},
{
"value": false,
"from": "default",
"field": "videoCompression"
},
{
"value": "cypress/videos",
"from": "default",
"field": "videosFolder"
},
{
"value": 500,
"from": "default",
"field": "viewportHeight"
},
{
"value": 500,
"from": "default",
"field": "viewportWidth"
},
{
"value": true,
"from": "default",
"field": "waitForAnimations"
},
{
"value": true,
"from": "default",
"field": "watchForFileChanges"
},
{
"value": "**/*.cy.{js,jsx,ts,tsx}",
"from": "default",
"field": "specPattern"
},
{
"value": [
{
"name": "chrome",
"family": "chromium",
"channel": "stable",
"displayName": "Chrome",
"version": "109.0.5414.119",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"minSupportedVersion": 64,
"majorVersion": "109"
},
{
"name": "firefox",
"family": "firefox",
"channel": "stable",
"displayName": "Firefox",
"version": "107.0.1",
"path": "/Applications/Firefox.app/Contents/MacOS/firefox",
"minSupportedVersion": 86,
"majorVersion": "107"
},
{
"name": "electron",
"channel": "stable",
"family": "chromium",
"displayName": "Electron",
"version": "106.0.5249.51",
"path": "",
"majorVersion": 106
}
],
"from": "runtime",
"field": "browsers"
},
{
"value": null,
"from": "default",
"field": "hosts"
},
{
"value": true,
"from": "default",
"field": "isInteractive"
}
],
"isFullConfigReady": true,
"hasNonExampleSpec": true,
"savedState": {
"firstOpened": 1674605493218,
"lastOpened": 1675067256771,
"lastProjectId": "7p5uce",
"specFilter": "",
"banners": {
"aci_082022_record": {
"lastShown": 1675061062102
}
}
},
"cloudProject": {
"__typename": "CloudProject",
"id": "Q2xvdWRQcm9qZWN0OjdwNXVjZQ==",
"runs": {
"nodes": [
{
"id": "Q2xvdWRSdW46bkdudmx5d3BHWg==",
"status": "PASSED",
"url": "https://cloud.cypress.io/projects/7p5uce/runs/2",
"__typename": "CloudRun"
},
{
"id": "Q2xvdWRSdW46YkxtdnhXWmpPUA==",
"status": "FAILED",
"url": "https://cloud.cypress.io/projects/7p5uce/runs/1",
"__typename": "CloudRun"
}
],
"__typename": "CloudRunConnection"
}
},
"__typename": "CurrentProject"
}
}
}

View File

@@ -1,656 +0,0 @@
{
"data": {
"currentProject": {
"id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
"title": "frontend",
"config": [
{
"value": 5,
"from": "default",
"field": "animationDistanceThreshold"
},
{
"value": "arm64",
"from": "default",
"field": "arch"
},
{
"value": null,
"from": "default",
"field": "baseUrl"
},
{
"value": null,
"from": "default",
"field": "blockHosts"
},
{
"value": true,
"from": "default",
"field": "chromeWebSecurity"
},
{
"value": [],
"from": "default",
"field": "clientCertificates"
},
{
"value": 4000,
"from": "default",
"field": "defaultCommandTimeout"
},
{
"value": "cypress/downloads",
"from": "default",
"field": "downloadsFolder"
},
{
"value": {
"INTERNAL_CLOUD_ENV": "production",
"INTERNAL_GRAPHQL_PORT": 4444,
"INTERNAL_EVENT_COLLECTOR_ENV": "staging",
"CONFIG_ENV": "production"
},
"field": "env",
"from": "env"
},
{
"value": 60000,
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalFetchPolyfill"
},
{
"value": false,
"from": "default",
"field": "experimentalInteractiveRunEvents"
},
{
"value": false,
"from": "default",
"field": "experimentalRunAllSpecs"
},
{
"value": false,
"from": "default",
"field": "experimentalMemoryManagement"
},
{
"value": false,
"from": "default",
"field": "experimentalModifyObstructiveThirdPartyCode"
},
{
"value": null,
"from": "default",
"field": "experimentalSkipDomainInjection"
},
{
"value": false,
"from": "default",
"field": "experimentalOriginDependencies"
},
{
"value": false,
"from": "default",
"field": "experimentalSourceRewriting"
},
{
"value": true,
"from": "config",
"field": "experimentalSingleTabRunMode"
},
{
"value": false,
"from": "default",
"field": "experimentalStudio"
},
{
"value": false,
"from": "default",
"field": "experimentalWebKitSupport"
},
{
"value": "",
"from": "default",
"field": "fileServerFolder"
},
{
"value": "cypress/fixtures",
"from": "default",
"field": "fixturesFolder"
},
{
"value": [
"**/__snapshots__/*",
"**/__image_snapshots__/*"
],
"from": "default",
"field": "excludeSpecPattern"
},
{
"value": false,
"from": "default",
"field": "includeShadowDom"
},
{
"value": 0,
"from": "default",
"field": "keystrokeDelay"
},
{
"value": true,
"from": "default",
"field": "modifyObstructiveCode"
},
{
"from": "default",
"field": "nodeVersion"
},
{
"value": 50,
"from": "default",
"field": "numTestsKeptInMemory"
},
{
"value": "darwin",
"from": "default",
"field": "platform"
},
{
"value": 60000,
"from": "default",
"field": "pageLoadTimeout"
},
{
"value": null,
"from": "default",
"field": "port"
},
{
"value": "vgqrwp",
"from": "config",
"field": "projectId"
},
{
"value": 20,
"from": "default",
"field": "redirectionLimit"
},
{
"value": "spec",
"from": "default",
"field": "reporter"
},
{
"value": null,
"from": "default",
"field": "reporterOptions"
},
{
"value": 5000,
"from": "default",
"field": "requestTimeout"
},
{
"value": null,
"from": "default",
"field": "resolvedNodePath"
},
{
"value": null,
"from": "default",
"field": "resolvedNodeVersion"
},
{
"value": 30000,
"from": "default",
"field": "responseTimeout"
},
{
"value": {
"runMode": 0,
"openMode": 0
},
"from": "default",
"field": "retries"
},
{
"value": true,
"from": "default",
"field": "screenshotOnRunFailure"
},
{
"value": "cypress/screenshots",
"from": "default",
"field": "screenshotsFolder"
},
{
"value": 250,
"from": "default",
"field": "slowTestThreshold"
},
{
"value": "top",
"from": "default",
"field": "scrollBehavior"
},
{
"value": "cypress/support/component.{js,jsx,ts,tsx}",
"from": "default",
"field": "supportFile"
},
{
"value": false,
"from": "default",
"field": "supportFolder"
},
{
"value": 60000,
"from": "default",
"field": "taskTimeout"
},
{
"value": true,
"from": "default",
"field": "testIsolation"
},
{
"value": true,
"from": "default",
"field": "trashAssetsBeforeRuns"
},
{
"value": null,
"from": "default",
"field": "userAgent"
},
{
"value": false,
"from": "default",
"field": "video"
},
{
"value": false,
"from": "default",
"field": "videoCompression"
},
{
"value": "cypress/videos",
"from": "default",
"field": "videosFolder"
},
{
"value": 500,
"from": "default",
"field": "viewportHeight"
},
{
"value": 500,
"from": "default",
"field": "viewportWidth"
},
{
"value": true,
"from": "default",
"field": "waitForAnimations"
},
{
"value": true,
"from": "default",
"field": "watchForFileChanges"
},
{
"value": "**/*.cy.{js,jsx,ts,tsx}",
"from": "default",
"field": "specPattern"
},
{
"value": [
{
"name": "chrome",
"family": "chromium",
"channel": "stable",
"displayName": "Chrome",
"version": "109.0.5414.119",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"minSupportedVersion": 64,
"majorVersion": "109"
},
{
"name": "firefox",
"family": "firefox",
"channel": "stable",
"displayName": "Firefox",
"version": "107.0.1",
"path": "/Applications/Firefox.app/Contents/MacOS/firefox",
"minSupportedVersion": 86,
"majorVersion": "107"
},
{
"name": "electron",
"channel": "stable",
"family": "chromium",
"displayName": "Electron",
"version": "106.0.5249.51",
"path": "",
"majorVersion": 106
}
],
"from": "runtime",
"field": "browsers"
},
{
"value": null,
"from": "default",
"field": "hosts"
},
{
"value": true,
"from": "default",
"field": "isInteractive"
}
],
"savedState": {
"firstOpened": 1674605493218,
"lastOpened": 1675053721981,
"lastProjectId": "vgqrwp",
"specFilter": ""
},
"currentTestingType": "component",
"branch": "main",
"packageManager": "yarn",
"activeBrowser": {
"id": "QnJvd3NlcjpjaHJvbWUtY2hyb21pdW0tc3RhYmxl",
"displayName": "Chrome",
"majorVersion": "109",
"__typename": "Browser"
},
"browsers": [
{
"id": "QnJvd3NlcjpjaHJvbWUtY2hyb21pdW0tc3RhYmxl",
"isSelected": true,
"displayName": "Chrome",
"version": "109.0.5414.119",
"majorVersion": "109",
"isVersionSupported": true,
"warning": null,
"disabled": null,
"__typename": "Browser"
},
{
"id": "QnJvd3NlcjpmaXJlZm94LWZpcmVmb3gtc3RhYmxl",
"isSelected": false,
"displayName": "Firefox",
"version": "107.0.1",
"majorVersion": "107",
"isVersionSupported": true,
"warning": null,
"disabled": null,
"__typename": "Browser"
},
{
"id": "QnJvd3NlcjplbGVjdHJvbi1jaHJvbWl1bS1zdGFibGU=",
"isSelected": false,
"displayName": "Electron",
"version": "106.0.5249.51",
"majorVersion": "106",
"isVersionSupported": true,
"warning": null,
"disabled": null,
"__typename": "Browser"
}
],
"projectId": "vgqrwp",
"cloudProject": {
"__typename": "CloudProject",
"id": "Q2xvdWRQcm9qZWN0OnZncXJ3cA=="
},
"__typename": "CurrentProject"
},
"isGlobalMode": true,
"versions": {
"current": {
"id": "12.4.0",
"version": "12.4.0",
"released": "2023-01-24T18:40:53.125Z",
"__typename": "Version"
},
"latest": {
"id": "12.4.1",
"version": "12.4.1",
"released": "2023-01-27T15:00:32.366Z",
"__typename": "Version"
},
"__typename": "VersionData"
},
"cloudViewer": {
"id": "Q2xvdWRVc2VyOjcxYTM3NmVhLTdlMGUtNDBhOS1hMTAzLWMwM2NmNTMyMmQyZg==",
"cloudOrganizationsUrl": "https://cloud.cypress.io/organizations",
"organizations": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"name": "Org 2",
"projects": {
"nodes": [],
"__typename": "CloudProjectConnection"
},
"__typename": "CloudOrganization"
},
{
"id": "Q2xvdWRPcmdhbml6YXRpb246MDIxZmVhNjctZDYwOC00YWIyLWFmMTctM2Y4YTJhMjNkMDE5",
"name": "Lachlan's Personal Projects",
"projects": {
"nodes": [
{
"id": "Q2xvdWRQcm9qZWN0OnZncXJ3cA==",
"slug": "vgqrwp",
"name": "Rhythm Game",
"__typename": "CloudProject"
}
],
"__typename": "CloudProjectConnection"
},
"__typename": "CloudOrganization"
},
{
"id": "Q2xvdWRPcmdhbml6YXRpb246ODllYmMwOTktNzhjMS00YjIzLWIwYzMtNjAzMGY0MjAxNDBj",
"name": "Lachlan Miller",
"projects": {
"nodes": [
{
"id": "Q2xvdWRQcm9qZWN0Om9mODhoNQ==",
"slug": "of88h5",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Onp5N2dzZQ==",
"slug": "zy7gse",
"name": "express",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmZ1aDkzOQ==",
"slug": "fuh939",
"name": "bannerjs",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjVicHF0MQ==",
"slug": "5bpqt1",
"name": "baretest88",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjJ5dm1odQ==",
"slug": "2yvmhu",
"name": "baretest414141",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Ojk4dzhveQ==",
"slug": "98w8oy",
"name": "desktop-gui-testing",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmJqdWJjYQ==",
"slug": "bjubca",
"name": "baretest58",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmQ4ZjM5bQ==",
"slug": "d8f39m",
"name": "baretest00",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmR3am5vMg==",
"slug": "dwjno2",
"name": "baretest66",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmZ3ZHZ1Mw==",
"slug": "fwdvu3",
"name": "31baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnVxNHhyYg==",
"slug": "uq4xrb",
"name": "baretest33331",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Ong5Y3BzOQ==",
"slug": "x9cps9",
"name": "555baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmZ6bW53Yw==",
"slug": "fzmnwc",
"name": "baretestdd",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnU5Y3d2Zg==",
"slug": "u9cwvf",
"name": "baretest-41",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Om9rZDQ3OA==",
"slug": "okd478",
"name": "baretest-1231",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjkxNTZiMw==",
"slug": "9156b3",
"name": "baretest555",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmlvbmNhbg==",
"slug": "ioncan",
"name": "baretest-asdf",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnpuYm9qOQ==",
"slug": "znboj9",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmljczdteA==",
"slug": "ics7mx",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OnN1cjRidw==",
"slug": "sur4bw",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjF1b2c1eA==",
"slug": "1uog5x",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Om52MXJ0OA==",
"slug": "nv1rt8",
"name": "baretest",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OmlnM2Nzaw==",
"slug": "ig3csk",
"name": "baretest-1",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjhlbWU2MQ==",
"slug": "8eme61",
"name": "rhythm-frontendddd",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0Ojk4anA1Ng==",
"slug": "98jp56",
"name": "rhythm-frontend",
"__typename": "CloudProject"
},
{
"id": "Q2xvdWRQcm9qZWN0OjNlNWJwYg==",
"slug": "3e5bpb",
"name": "Lachlan Miller Testing",
"__typename": "CloudProject"
}
],
"__typename": "CloudProjectConnection"
},
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"email": "lachlan.miller.1990@outlook.com",
"fullName": "Lachlan Miller",
"firstOrganization": {
"nodes": [
{
"id": "Q2xvdWRPcmdhbml6YXRpb246NjE5ODJiMmItOTRmNy00ZjYzLTlmYjctNGI1MTc4NjQ5OWJh",
"__typename": "CloudOrganization"
}
],
"__typename": "CloudOrganizationConnection"
},
"__typename": "CloudUser"
},
"authState": {
"browserOpened": false,
"name": null,
"message": null,
"__typename": "AuthState"
},
"cachedUser": {
"id": "Q2FjaGVkVXNlcjpsYWNobGFuLm1pbGxlci4xOTkwQG91dGxvb2suY29t",
"fullName": "Lachlan Miller",
"email": "lachlan.miller.1990@outlook.com",
"__typename": "CachedUser"
}
}
}

View File

@@ -1,11 +0,0 @@
{
"data": {
"baseError": null,
"currentProject": {
"id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
"isLoadingConfigFile": false,
"isLoadingNodeEvents": false,
"__typename": "CurrentProject"
}
}
}

View File

@@ -1,35 +0,0 @@
{
"data": {
"localSettings": {
"preferences": {
"isSideNavigationOpen": true,
"isSpecsListOpen": false,
"autoScrollingEnabled": false,
"reporterWidth": 618,
"specListWidth": null,
"__typename": "LocalSettingsPreferences"
},
"__typename": "LocalSettings"
},
"currentProject": {
"id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
"cloudProject": {
"__typename": "CloudProject",
"id": "Q2xvdWRQcm9qZWN0OjdwNXVjZQ==",
"runByNumber": {
"id": "Q2xvdWRSdW46bkdudmx5d3BHWg==",
"status": "PASSED",
"totalFailed": 0,
"__typename": "CloudRun"
}
},
"isCTConfigured": true,
"isE2EConfigured": true,
"currentTestingType": "component",
"title": "frontend",
"branch": "main",
"__typename": "CurrentProject"
},
"invokedFromCli": true
}
}

View File

@@ -1,10 +0,0 @@
{
"data": {
"currentProject": {
"id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
"branch": "main",
"projectId": "7p5uce",
"__typename": "CurrentProject"
}
}
}

View File

@@ -27,7 +27,6 @@ type NonNullCloudSpec = Exclude<SpecsListFragment['cloudSpec'], undefined | null
export function useCloudSpecData (
isProjectDisconnected: Ref<boolean>,
isOffline: Ref<boolean>,
projectId: string | null | undefined,
mostRecentUpdate: Ref<string | null>,
displayedSpecs: Ref<(SpecsListFragment | undefined)[]>,
allSpecs: (SpecsListFragment | undefined)[],

View File

@@ -13,9 +13,15 @@ import { uniq } from 'lodash'
* subscriptions had ended when the component it was registered in was unmounted.
*/
gql`
fragment UseRelevantRun on RelevantRun {
all {
runId
runNumber
sha
status
}
latest {
runId
runNumber
sha
status
@@ -42,7 +48,7 @@ gql`
`
export function useRelevantRun (location: 'SIDEBAR' | 'DEBUG') {
export function useRelevantRun (location: 'SIDEBAR' | 'DEBUG' | 'RUNS' | 'SPECS') {
const userProjectStatusStore = useUserProjectStatusStore()
const shouldPause = computed(() => {
@@ -68,6 +74,7 @@ export function useRelevantRun (location: 'SIDEBAR' | 'DEBUG') {
return {
all: subscriptionResponse.data.value?.relevantRuns?.all,
latest: subscriptionResponse.data.value?.relevantRuns?.latest,
commitsAhead: subscriptionResponse.data.value?.relevantRuns?.commitsAhead,
selectedRun,
commitShas,

View File

@@ -5,6 +5,7 @@
<RunsContainer
v-else
:gql="query.data.value"
:runs="runs"
:online="isOnlineRef"
data-cy="runs-container"
@re-execute-runs-query="reExecuteRunsQuery"
@@ -14,24 +15,31 @@
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
import { gql, useQuery } from '@urql/vue'
import { RunsDocument } from '../generated/graphql'
import { Ref, ref, watchEffect } from 'vue'
import RunsSkeleton from '../runs/RunsSkeleton.vue'
import RunsContainer from '../runs/RunsContainer.vue'
import TransitionQuickFade from '@cy/components/transitions/TransitionQuickFade.vue'
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
import { useOnline } from '@vueuse/core'
gql`
query Runs {
...RunsContainer
}`
const query = useQuery({ query: RunsDocument, requestPolicy: 'network-only' })
import { useProjectRuns } from '../runs/useProjectRuns'
import { useGitTreeRuns } from '../runs/useGitTreeRuns'
import type { RunsComposable } from '../runs/RunsComposable'
const isOnlineRef = ref(true)
const online = useOnline()
const isUsingGit = useUserProjectStatusStore().project.isUsingGit
let runComposable: (online: Ref<boolean>) => RunsComposable
if (isUsingGit) {
runComposable = useGitTreeRuns
} else {
runComposable = useProjectRuns
}
const { runs, reExecuteRunsQuery, query } = runComposable(isOnlineRef)
watchEffect(() => {
// We want to keep track of the previous state to refetch the query
// when the internet connection is back
@@ -41,11 +49,8 @@ watchEffect(() => {
if (online.value && !isOnlineRef.value) {
isOnlineRef.value = true
query.executeQuery()
reExecuteRunsQuery()
}
})
function reExecuteRunsQuery () {
query.executeQuery()
}
</script>

View File

@@ -21,18 +21,27 @@
:is-default-spec-pattern="isDefaultSpecPattern"
@showCreateSpecModal="showCreateSpecModal"
/>
<SpecsListRunWatcher
v-for="run in latestRuns"
:key="run.runId"
:run="run"
@run-update="runUpdate()"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { gql, SubscriptionHandlerArg, useQuery } from '@urql/vue'
import { computed, ref, watch } from 'vue'
import { gql, useQuery } from '@urql/vue'
import { useI18n } from '@cy/i18n'
import SpecsList from '../../specs/SpecsList.vue'
import NoSpecsPage from '../../specs/NoSpecsPage.vue'
import CreateSpecModal from '../../specs/CreateSpecModal.vue'
import { SpecsPageContainerDocument, SpecsPageContainer_SpecsChangeDocument, SpecsPageContainer_SpecListPollingDocument, SpecsPageContainer_BranchInfoDocument } from '../../generated/graphql'
import SpecsListRunWatcher from '../../specs/SpecsListRunWatcher.vue'
import { SpecsPageContainerDocument, SpecsPageContainer_SpecsChangeDocument } from '../../generated/graphql'
import { useSubscription } from '../../graphql'
import { useRelevantRun } from '../../composables/useRelevantRun'
import { isEmpty } from 'lodash'
const { t } = useI18n()
@@ -47,7 +56,7 @@ query SpecsPageContainer_BranchInfo {
`
gql`
query SpecsPageContainer($fromBranch: String!, $hasBranch: Boolean!) {
query SpecsPageContainer($runIds: [ID!]!, $hasRunIds: Boolean!) {
...Specs_SpecsList
...NoSpecsPage
...CreateSpecModal
@@ -59,37 +68,24 @@ query SpecsPageContainer($fromBranch: String!, $hasBranch: Boolean!) {
`
gql`
subscription SpecsPageContainer_specsChange($fromBranch: String!, $hasBranch: Boolean!) {
subscription SpecsPageContainer_specsChange($runIds: [ID!]!, $hasRunIds: Boolean!) {
specsChange {
id
specs {
id
...SpecsList
...SpecsList
}
}
}
`
gql`
subscription SpecsPageContainer_specListPolling($fromBranch: String, $projectId: String) {
startPollingForSpecs(branchName: $fromBranch, projectId: $projectId)
}
`
const branchInfo = useQuery({ query: SpecsPageContainer_BranchInfoDocument })
const relevantRuns = useRelevantRun('SPECS')
const variables = computed(() => {
const fromBranch = branchInfo.data.value?.currentProject?.branch ?? ''
const hasBranch = Boolean(fromBranch)
const runIds = relevantRuns.value.latest?.map((run) => run.runId) || []
const hasRunIds = !isEmpty(runIds)
return { fromBranch, hasBranch }
})
const pollingVariables = computed(() => {
const fromBranch = branchInfo.data.value?.currentProject?.branch ?? null
const projectId = branchInfo.data.value?.currentProject?.projectId ?? null
return { fromBranch, projectId }
return { runIds, hasRunIds }
})
useSubscription({
@@ -97,17 +93,29 @@ useSubscription({
variables,
})
const mostRecentUpdate = ref<string|null>(null)
/**
* Used to trigger Spec updates via the useCloudSpec composable.
*/
const mostRecentUpdate = ref<string | undefined>()
const updateMostRecentUpdate: SubscriptionHandlerArg<any, any> = (_, reportedUpdate) => {
mostRecentUpdate.value = reportedUpdate?.startPollingForSpecs ?? null
/**
* At this time, the CloudRun is not passing the `updatedAt` field. To mimic
* that, we are setting the current date/time here each time any of the runs change.
*/
watch(() => relevantRuns.value, (value, oldValue) => {
if (value && oldValue && value.all !== oldValue.all) {
runUpdate()
}
})
const latestRuns = computed(() => {
return relevantRuns.value.latest
})
const runUpdate = () => {
mostRecentUpdate.value = new Date().toISOString()
}
useSubscription({
query: SpecsPageContainer_SpecListPollingDocument,
variables: pollingVariables,
}, updateMostRecentUpdate)
const query = useQuery({
query: SpecsPageContainerDocument,
variables,

View File

@@ -751,7 +751,13 @@ export class EventManager {
* Return it's response.
*/
Cypress.primaryOriginCommunicator.on('backend:request', async ({ args }, { source, responseEvent }) => {
const response = await Cypress.backend(...args)
let response
try {
response = await Cypress.backend(...args)
} catch (error) {
response = { error }
}
Cypress.primaryOriginCommunicator.toSource(source, responseEvent, response)
})

View File

@@ -152,13 +152,25 @@ function setupRunner () {
createIframeModel()
}
interface GetSpecUrlOptions {
browserFamily?: string
namespace: string
specSrc: string
}
/**
* Get the URL for the spec. This is the URL of the AUT IFrame.
* CT uses absolute URLs, and serves from the dev server.
* E2E uses relative, serving from our internal server's spec controller.
*/
function getSpecUrl (namespace: string, specSrc: string) {
return `/${namespace}/iframes/${specSrc}`
function getSpecUrl ({ browserFamily, namespace, specSrc }: GetSpecUrlOptions) {
let url = `/${namespace}/iframes/${specSrc}`
if (browserFamily) {
url += `?browserFamily=${browserFamily}`
}
return url
}
/**
@@ -202,13 +214,15 @@ export function addCrossOriginIframe (location) {
return
}
const config = getRunnerConfigFromWindow()
addIframe({
id,
// the cross origin iframe is added to the document body instead of the
// container since it needs to match the size of the top window for screenshots
$container: document.body,
className: 'spec-bridge-iframe',
src: `${location.origin}/${getRunnerConfigFromWindow().namespace}/spec-bridge-iframes`,
src: `${location.origin}/${config.namespace}/spec-bridge-iframes?browserFamily=${config.browser.family}`,
})
}
@@ -234,7 +248,10 @@ function runSpecCT (config, spec: SpecFile) {
const autIframe = getAutIframeModel()
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
const specSrc = getSpecUrl(config.namespace, spec.absolute)
const specSrc = getSpecUrl({
namespace: config.namespace,
specSrc: spec.absolute,
})
autIframe._showInitialBlankPage()
$autIframe.prop('src', specSrc)
@@ -297,7 +314,11 @@ function runSpecE2E (config, spec: SpecFile) {
autIframe.visitBlankPage()
// create Spec IFrame
const specSrc = getSpecUrl(config.namespace, encodeURIComponent(spec.relative))
const specSrc = getSpecUrl({
browserFamily: config.browser.family,
namespace: config.namespace,
specSrc: encodeURIComponent(spec.relative),
})
// FIXME: BILL Determine where to call client with to force browser repaint
/**

View File

@@ -70,9 +70,9 @@
import { computed } from 'vue'
import ListRowHeader from '@cy/components/ListRowHeader.vue'
import ExternalLink from '@cy/gql-components/ExternalLink.vue'
import { gql } from '@urql/core'
import { gql, useSubscription } from '@urql/vue'
import RunResults from './RunResults.vue'
import type { CloudRunStatus, RunCardFragment } from '../generated/graphql'
import { CloudRunStatus, RunCardFragment, RunCard_ChangeDocument } from '../generated/graphql'
import { dayjs } from './utils/day.js'
import { useDurationFormat } from '../composables/useDurationFormat'
import { SolidStatusIcon, StatusType } from '@cypress-design/vue-statusicon'
@@ -99,6 +99,15 @@ fragment RunCard on CloudRun {
}
`
gql`
subscription RunCard_Change($id: ID!) {
relevantRunSpecChange(runId: $id) {
id
...RunCard
}
}
`
const STATUS_MAP: Record<CloudRunStatus, StatusType> = {
PASSED: 'passed',
FAILED: 'failed',
@@ -116,6 +125,18 @@ const props = defineProps<{
const run = computed(() => props.gql)
const subscriptionVariables = computed(() => {
return {
id: run.value.id,
}
})
const shouldPauseSubscription = computed(() => {
return run.value.status !== 'RUNNING'
})
useSubscription({ query: RunCard_ChangeDocument, variables: subscriptionVariables, pause: shouldPauseSubscription })
const runUrl = computed(() => {
return getUrlWithParams({
url: run.value.url || '#',

View File

@@ -0,0 +1,8 @@
import type { Ref } from 'vue'
import type { RunCardFragment } from '../generated/graphql'
export type RunsComposable = {
runs: Ref<RunCardFragment[] | undefined>
reExecuteRunsQuery: () => void
query: any
}

View File

@@ -1,19 +1,10 @@
import RunsContainer from './RunsContainer.vue'
import { RunsContainerFragmentDoc } from '../generated/graphql-test'
import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes'
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
import { defaultMessages } from '@cy/i18n'
describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
const cloudViewer = {
...CloudUserStubs.me,
organizations: null,
firstOrganization: {
nodes: [],
},
}
context('when the user is logged in', () => {
beforeEach(() => {
const userProjectStatusStore = useUserProjectStatusStore()
@@ -23,11 +14,10 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
it('renders with expected runs if there is a cloud project id', () => {
cy.mountFragment(RunsContainerFragmentDoc, {
onResult: (result) => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <RunsContainer gql={gqlVal} online />
const runs = gqlVal.currentProject?.cloudProject?.__typename === 'CloudProject' ? gqlVal.currentProject.cloudProject.runs?.nodes : undefined
return <RunsContainer gql={gqlVal} runs={runs} online />
},
})
@@ -43,7 +33,6 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
it('renders instructions and "connect" link without a project id', () => {
cy.mountFragment(RunsContainerFragmentDoc, {
onResult: (result) => {
result.cloudViewer = cloudViewer
if (result.currentProject?.projectId) {
result.currentProject.projectId = ''
}
@@ -79,19 +68,12 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
context('when the user has no recorded runs', () => {
it('renders instructions and record prompt', () => {
const userProjectStatusStore = useUserProjectStatusStore()
userProjectStatusStore.setUserFlag('isLoggedIn', true)
cy.mountFragment(RunsContainerFragmentDoc, {
onResult (gql) {
gql.cloudViewer = cloudViewer
if (gql.currentProject?.cloudProject?.__typename === 'CloudProject') {
gql.currentProject.cloudProject.runs = {
__typename: 'CloudRunConnection',
pageInfo: null as any,
nodes: [],
}
}
},
render (gqlVal) {
return <RunsContainer gql={gqlVal} online />
return <RunsContainer gql={gqlVal} runs={[]} online />
},
})
@@ -103,9 +85,11 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
context('with errors', () => {
it('renders connection failed', () => {
const userProjectStatusStore = useUserProjectStatusStore()
userProjectStatusStore.setUserFlag('isLoggedIn', true)
cy.mountFragment(RunsContainerFragmentDoc, {
onResult (result) {
result.cloudViewer = cloudViewer
result.currentProject!.cloudProject = null
},
render (gqlVal) {
@@ -134,11 +118,11 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
userProjectStatusStore.setUserFlag('isLoggedIn', true)
cy.mountFragment(RunsContainerFragmentDoc, {
onResult: (result) => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <RunsContainer gql={gqlVal} online />
const cloudProject = gqlVal.currentProject?.cloudProject?.__typename === 'CloudProject' ? gqlVal.currentProject.cloudProject : undefined
const runs = cloudProject?.runs ? cloudProject.runs.nodes : undefined
return <RunsContainer gql={gqlVal} runs={runs} online />
},
})
@@ -153,11 +137,11 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
userProjectStatusStore.setUserFlag('isLoggedIn', true)
cy.mountFragment(RunsContainerFragmentDoc, {
onResult: (result) => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <RunsContainer gql={gqlVal} online />
const cloudProject = gqlVal.currentProject?.cloudProject?.__typename === 'CloudProject' ? gqlVal.currentProject.cloudProject : undefined
const runs = cloudProject?.runs ? cloudProject.runs.nodes : undefined
return <RunsContainer gql={gqlVal} runs={runs} online />
},
})
@@ -180,13 +164,13 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
setProjectFlag('isConfigLoaded', true)
setProjectFlag('isUsingGit', true)
expect(cloudStatusMatches('needsRecordedRun')).equals(true)
expect(cloudStatusMatches('needsRecordedRun'), 'status should be needsRecordedRun').equals(true)
cy.mountFragment(RunsContainerFragmentDoc, {
onResult: (result) => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <RunsContainer gql={gqlVal} online />
const cloudProject = gqlVal.currentProject?.cloudProject?.__typename === 'CloudProject' ? gqlVal.currentProject.cloudProject : undefined
const runs = cloudProject?.runs ? cloudProject.runs.nodes : undefined
return <RunsContainer gql={gqlVal} runs={runs} online />
},
})
@@ -209,11 +193,11 @@ describe('<RunsContainer />', { keystrokeDelay: 0 }, () => {
expect(cloudStatusMatches('needsRecordedRun')).equals(true)
cy.mountFragment(RunsContainerFragmentDoc, {
onResult: (result) => {
result.cloudViewer = cloudViewer
},
render (gqlVal) {
return <RunsContainer gql={gqlVal} online />
const cloudProject = gqlVal.currentProject?.cloudProject?.__typename === 'CloudProject' ? gqlVal.currentProject.cloudProject : undefined
const runs = cloudProject?.runs ? cloudProject.runs.nodes : undefined
return <RunsContainer gql={gqlVal} runs={runs} online />
},
})

View File

@@ -6,12 +6,12 @@
<RunsConnectSuccessAlert
v-if="currentProject && showConnectSuccessAlert"
:gql="currentProject"
:class="{ 'absolute left-[24px] right-[24px] top-[24px]': currentProject?.cloudProject?.__typename === 'CloudProject' && !currentProject.cloudProject.runs?.nodes.length }"
:class="{ 'absolute left-[24px] right-[24px] top-[24px]': currentProject?.cloudProject?.__typename === 'CloudProject' && !runs?.length }"
/>
<RunsConnect
v-if="!currentProject?.projectId || !cloudViewer?.id"
:campaign="!cloudViewer?.id ? RUNS_PROMO_CAMPAIGNS.login : RUNS_PROMO_CAMPAIGNS.connectProject"
v-if="!userProjectStatusStore.user.isLoggedIn || !currentProject?.projectId"
:campaign="!userProjectStatusStore.user.isLoggedIn ? RUNS_PROMO_CAMPAIGNS.login : RUNS_PROMO_CAMPAIGNS.connectProject"
/>
<RunsErrorRenderer
v-else-if="currentProject?.cloudProject?.__typename !== 'CloudProject' || connectionFailed"
@@ -20,7 +20,7 @@
/>
<RunsEmpty
v-else-if="!currentProject?.cloudProject?.runs?.nodes.length"
v-else-if="!runs?.length"
/>
<div
v-else
@@ -61,7 +61,7 @@
{{ t('runs.empty.ensureGitSetupCorrectly') }}
</TrackedBanner>
<RunCard
v-for="run of currentProject?.cloudProject?.runs?.nodes"
v-for="run of runs"
:key="run.id"
:gql="run"
/>
@@ -70,15 +70,13 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { gql, useMutation } from '@urql/vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from '@cy/i18n'
import NoInternetConnection from '@packages/frontend-shared/src/components/NoInternetConnection.vue'
import RunCard from './RunCard.vue'
import RunsConnect from './RunsConnect.vue'
import RunsConnectSuccessAlert from './RunsConnectSuccessAlert.vue'
import RunsEmpty from './RunsEmpty.vue'
import { RunsContainerFragment, RunsContainer_FetchNewerRunsDocument } from '../generated/graphql'
import Warning from '@packages/frontend-shared/src/warning/Warning.vue'
import RunsErrorRenderer from './RunsErrorRenderer.vue'
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
@@ -88,6 +86,7 @@ import { getUtmSource } from '@packages/frontend-shared/src/utils/getUtmSource'
import TrackedBanner from '../specs/banners/TrackedBanner.vue'
import { BannerIds } from '@packages/types/src'
import { useMarkdown } from '@packages/frontend-shared/src/composables/useMarkdown'
import type { RunCardFragment, RunsContainerFragment, RunsGitTreeQuery } from '../generated/graphql'
const { t } = useI18n()
@@ -99,128 +98,11 @@ const emit = defineEmits<{
(e: 'reExecuteRunsQuery'): void
}>()
gql`
fragment RunsContainer_RunsConnection on CloudRunConnection {
nodes {
id
...RunCard
}
pageInfo {
startCursor
}
}
`
gql`
fragment RunsContainer on Query {
...RunsErrorRenderer
currentProject {
id
projectId
...RunsConnectSuccessAlert
cloudProject {
__typename
... on CloudProject {
id
runs(first: 10) {
...RunsContainer_RunsConnection
}
}
}
}
cloudViewer {
id
}
}`
gql`
mutation RunsContainer_FetchNewerRuns(
$cloudProjectNodeId: ID!,
$beforeCursor: String,
$hasBeforeCursor: Boolean!,
$refreshPendingRuns: [ID!]!,
$hasRefreshPendingRuns: Boolean!
) {
refetchRemote {
cloudNode(id: $cloudProjectNodeId) {
id
__typename
... on CloudProject {
runs(first: 10) @skip(if: $hasBeforeCursor) {
...RunsContainer_RunsConnection
}
newerRuns: runs(last: 10, before: $beforeCursor) @include(if: $hasBeforeCursor) {
...RunsContainer_RunsConnection
}
}
}
cloudNodesByIds(ids: $refreshPendingRuns) @include(if: $hasRefreshPendingRuns) {
id
... on CloudRun {
...RunCard
}
}
}
}
`
const currentProject = computed(() => props.gql.currentProject)
const cloudViewer = computed(() => props.gql.cloudViewer)
const variables = computed(() => {
if (currentProject.value?.cloudProject?.__typename === 'CloudProject') {
const toRefresh = currentProject.value?.cloudProject.runs?.nodes?.map((r) => r.status === 'RUNNING' ? r.id : null).filter((f) => f) ?? []
return {
cloudProjectNodeId: currentProject.value?.cloudProject.id,
beforeCursor: currentProject.value?.cloudProject.runs?.pageInfo.startCursor,
hasBeforeCursor: Boolean(currentProject.value?.cloudProject.runs?.pageInfo.startCursor),
refreshPendingRuns: toRefresh,
hasRefreshPendingRuns: toRefresh.length > 0,
}
}
return undefined as any
})
const refetcher = useMutation(RunsContainer_FetchNewerRunsDocument)
// 15 seconds polling
const POLL_FOR_LATEST = 1000 * 15
let timeout: null | number = null
function startPolling () {
timeout = window.setTimeout(function fetchNewerRuns () {
if (variables.value && props.online) {
refetcher.executeMutation(variables.value)
.then(() => {
startPolling()
})
} else {
startPolling()
}
}, POLL_FOR_LATEST)
}
onMounted(() => {
// Always fetch when the component mounts, and we're not already fetching
if (props.online && !refetcher.fetching) {
refetcher.executeMutation(variables.value)
}
startPolling()
})
onUnmounted(() => {
if (timeout) {
clearTimeout(timeout)
}
timeout = null
})
const props = defineProps<{
gql: RunsContainerFragment
gql: RunsContainerFragment | RunsGitTreeQuery
runs?: RunCardFragment[]
online: boolean
}>()

View File

@@ -0,0 +1,64 @@
import { gql, useQuery } from '@urql/vue'
import { Ref, computed } from 'vue'
import { RunsGitTreeDocument, RunCardFragment } from '../generated/graphql'
import { useRelevantRun } from '../composables/useRelevantRun'
import type { RunsComposable } from './RunsComposable'
gql`
query RunsGitTree($runIds: [ID!]!) {
...RunsGitTreeProject
}
`
gql `
fragment RunsGitTreeProject on Query {
...RunsErrorRenderer
currentProject {
id
projectId
...RunsConnectSuccessAlert
cloudProject {
__typename
... on CloudProject {
id
}
}
}
cloudNodesByIds(ids: $runIds) {
id
...RunCard
}
}
`
export const useGitTreeRuns = (online: Ref<boolean>): RunsComposable => {
const relevantRuns = useRelevantRun('RUNS')
const variables = computed(() => {
return {
runIds: relevantRuns?.value.latest?.map((run) => run.runId) || [],
}
})
const shouldPauseQuery = computed(() => {
return !variables.value.runIds
})
const query = useQuery({ query: RunsGitTreeDocument, variables, pause: shouldPauseQuery, requestPolicy: 'network-only' })
const runs = computed(() => {
const nodes = query.data.value?.cloudNodesByIds?.filter((val): val is RunCardFragment => val?.__typename === 'CloudRun')
return nodes
})
function reExecuteRunsQuery () {
query.executeQuery()
}
return {
runs,
reExecuteRunsQuery,
query,
}
}

View File

@@ -0,0 +1,128 @@
import { gql, useMutation, useQuery } from '@urql/vue'
import { Ref, computed, onMounted, onUnmounted } from 'vue'
import { RunsDocument, RunsContainer_FetchNewerRunsDocument, RunCardFragment } from '../generated/graphql'
import type { RunsComposable } from './RunsComposable'
gql`
query Runs {
...RunsContainer
}`
gql`
fragment RunsContainer_RunsConnection on CloudRunConnection {
nodes {
id
...RunCard
}
pageInfo {
startCursor
}
}
`
gql`
fragment RunsContainer on Query {
...RunsErrorRenderer
currentProject {
id
projectId
...RunsConnectSuccessAlert
cloudProject {
__typename
... on CloudProject {
id
runs(first: 10) {
...RunsContainer_RunsConnection
}
}
}
}
}`
gql`
mutation RunsContainer_FetchNewerRuns(
$cloudProjectNodeId: ID!,
$beforeCursor: String,
$hasBeforeCursor: Boolean!,
) {
refetchRemote {
cloudNode(id: $cloudProjectNodeId) {
id
__typename
... on CloudProject {
runs(first: 10) @skip(if: $hasBeforeCursor) {
...RunsContainer_RunsConnection
}
newerRuns: runs(last: 10, before: $beforeCursor) @include(if: $hasBeforeCursor) {
...RunsContainer_RunsConnection
}
}
}
}
}
`
export const useProjectRuns = (online: Ref<boolean>): RunsComposable => {
const query = useQuery({ query: RunsDocument, requestPolicy: 'network-only' })
const currentProject = computed(() => query.data.value?.currentProject)
const runs = computed(() => query.data.value?.currentProject?.cloudProject?.__typename === 'CloudProject' ? query.data.value?.currentProject?.cloudProject?.runs?.nodes as RunCardFragment[] : [])
const variables = computed(() => {
if (currentProject.value?.cloudProject?.__typename === 'CloudProject') {
return {
cloudProjectNodeId: currentProject.value?.cloudProject.id,
beforeCursor: currentProject.value?.cloudProject.runs?.pageInfo.startCursor,
hasBeforeCursor: Boolean(currentProject.value?.cloudProject.runs?.pageInfo.startCursor),
}
}
return undefined as any
})
const refetcher = useMutation(RunsContainer_FetchNewerRunsDocument)
// 15 seconds polling
const POLL_FOR_LATEST = 1000 * 15
let timeout: null | number = null
function startPolling () {
timeout = window.setTimeout(function fetchNewerRuns () {
if (variables.value && online.value) {
refetcher.executeMutation(variables.value)
.then(() => {
startPolling()
})
} else {
startPolling()
}
}, POLL_FOR_LATEST)
}
onMounted(() => {
// Always fetch when the component mounts, and we're not already fetching
if (online.value && !refetcher.fetching) {
refetcher.executeMutation(variables.value)
}
startPolling()
})
onUnmounted(() => {
if (timeout) {
clearTimeout(timeout)
}
timeout = null
})
function reExecuteRunsQuery () {
query.executeQuery()
}
return {
runs,
reExecuteRunsQuery,
query,
}
}

View File

@@ -8,7 +8,7 @@ describe('<AverageDuration />', () => {
data: {
__typename: 'CloudProjectSpec',
id: 'id',
averageDuration: duration,
averageDurationForRunIds: duration,
retrievedAt: new Date().toISOString(),
},
}

View File

@@ -1,10 +1,10 @@
<template>
<div
v-if="props.gql?.data?.__typename === 'CloudProjectSpec' && props.gql?.data?.averageDuration"
v-if="props.gql?.data?.__typename === 'CloudProjectSpec' && props.gql?.data?.averageDurationForRunIds"
class="h-full grid text-gray-700 justify-end items-center"
data-cy="average-duration"
>
{{ getDurationString(props.gql.data.averageDuration) }}
{{ getDurationString(props.gql.data.averageDurationForRunIds) }}
</div>
<div
v-else
@@ -30,7 +30,7 @@ fragment AverageDuration on RemoteFetchableCloudProjectSpecResult {
... on CloudProjectSpec {
id
retrievedAt
averageDuration(fromBranch: $fromBranch)
averageDurationForRunIds(cloudRunIds: $runIds)
}
}
}

View File

@@ -11,11 +11,9 @@ function mountWithRuns (runs: Required<CloudSpecRun>[]) {
__typename: 'CloudProjectSpec',
retrievedAt: new Date().toISOString(),
id: 'id',
specRuns: {
nodes: [
...runs as any, // suppress TS compiler
],
},
specRunsForRunIds: [
...runs as any, // suppress TS compiler
],
},
}

View File

@@ -92,9 +92,6 @@ import SpecRunSummary from './SpecRunSummary.vue'
import { gql } from '@urql/vue'
import { getUrlWithParams } from '@packages/frontend-shared/src/utils/getUrlWithParams'
// cloudProjectSpec.specRuns was marked deprecated in the cloud in favor of a new
// field. When the work is completed to use that field, remove this eslist-disable comment
/* eslint-disable graphql/no-deprecated-fields */
gql`
fragment RunStatusDots on RemoteFetchableCloudProjectSpecResult {
id
@@ -108,38 +105,36 @@ fragment RunStatusDots on RemoteFetchableCloudProjectSpecResult {
... on CloudProjectSpec {
id
retrievedAt
specRuns(first: 4, fromBranch: $fromBranch) {
nodes {
id
runNumber
basename
path
extension
testsFailed{
min
max
}
testsPassed{
min
max
}
testsPending{
min
max
}
testsSkipped{
min
max
}
createdAt
groupCount
specDuration{
min
max
}
status
url
specRunsForRunIds(cloudRunIds: $runIds) {
id
runNumber
basename
path
extension
testsFailed{
min
max
}
testsPassed{
min
max
}
testsPending{
min
max
}
testsSkipped{
min
max
}
createdAt
groupCount
specDuration{
min
max
}
status
url
}
}
}
@@ -153,7 +148,7 @@ const props = defineProps<{
}>()
const runs = computed(() => {
return props.gql?.data?.__typename === 'CloudProjectSpec' ? props.gql?.data?.specRuns?.nodes ?? [] : []
return props.gql?.data?.__typename === 'CloudProjectSpec' ? props.gql?.data?.specRunsForRunIds ?? [] : []
})
const isRunsLoaded = computed(() => {

View File

@@ -11,10 +11,10 @@ describe('<SpecsList />', { keystrokeDelay: 0 }, () => {
return cy.mountFragment(Specs_SpecsListFragmentDoc, {
variableTypes: {
hasBranch: 'Boolean',
hasRunIds: 'Boolean',
},
variables: {
hasBranch: true,
hasRunIds: false,
},
onResult: (ctx) => {
if (!ctx.currentProject) throw new Error('need current project')

View File

@@ -297,7 +297,7 @@ fragment SpecsList on Spec {
gitInfo {
...SpecListRow
}
cloudSpec(name: "cloudSpec") @include(if: $hasBranch) {
cloudSpec(name: "cloudSpec") @include(if: $hasRunIds) {
id
fetchingStatus
...AverageDuration
@@ -432,7 +432,6 @@ const mostRecentUpdateRef = toRef(props, 'mostRecentUpdate')
const { refetchFailedCloudData } = useCloudSpecData(
isProjectDisconnected,
isOffline,
props.gql.currentProject?.projectId,
mostRecentUpdateRef,
displayedSpecs,
props.gql.currentProject?.specs as SpecsListFragment[] || [],

View File

@@ -0,0 +1,50 @@
/**
Non rendering component used to watch running runs and emit an event
when a run has changed. Used by the specs list to update spec information
during a RUNNING run.
*/
<script setup lang="ts">
import { gql, useSubscription } from '@urql/vue'
import { computed } from 'vue'
import type { CloudRunStatus } from '../generated/graphql'
import { SpecsListRunWatcherDocument } from '../generated/graphql'
/**
* Subscription to watch a run.
* Values being queried are there to tell the polling source
* which fields to watch for changes
*/
gql`
subscription SpecsListRunWatcher($id: ID!) {
relevantRunSpecChange(runId: $id) {
id
status
totalInstanceCount
completedInstanceCount
}
}
`
const props = defineProps<{
run: {runId: string, status: CloudRunStatus | null }
}>()
const emits = defineEmits<{
(eventName: 'runUpdate'): void
}>()
const variables = computed(() => {
return { id: props.run.runId }
})
const shouldPause = computed(() => {
return props.run.status !== 'RUNNING'
})
const handleUpdate = () => {
emits('runUpdate')
}
useSubscription({ query: SpecsListRunWatcherDocument, variables, pause: shouldPause }, handleUpdate)
</script>

View File

@@ -22,10 +22,14 @@ describe('<FlakyInformation />', () => {
data: {
__typename: 'CloudProjectSpec',
id: '3',
isConsideredFlaky: flaky,
flakyStatus: {
isConsideredFlakyForRunIds: flaky,
flakyStatusForRunIds: {
__typename: 'CloudProjectSpecFlakyStatus',
dashboardUrl: '#',
flakyRuns: 1,
flakyRunsWindow: 5,
lastFlaky: 3,
severity: 'LOW',
},
},
}

View File

@@ -20,12 +20,13 @@
:href="cloudUrl"
class="hocus:no-underline"
>
<FlakySpecSummaryAdapter
:project-id="props.projectGql.projectId"
:from-branch="props.projectGql?.branch || ''"
:spec-path="props.specGql.relative"
<FlakySpecSummary
:spec-name="props.specGql?.fileName ?? ''"
:spec-extension="props.specGql?.specFileExtension ?? ''"
:severity="flakyStatus?.severity ?? 'NONE'"
:total-runs="flakyStatus?.flakyRunsWindow ?? 0"
:total-flaky-runs="flakyStatus?.flakyRuns ?? 0"
:runs-since-last-flake="flakyStatus?.lastFlaky ?? 0"
/>
</ExternalLink>
</template>
@@ -39,8 +40,8 @@ import type { FlakyInformationProjectFragment, FlakyInformationSpecFragment, Fla
import { gql } from '@urql/vue'
import { computed } from 'vue'
import Tooltip from '@packages/frontend-shared/src/components/Tooltip.vue'
import FlakySpecSummaryAdapter from './FlakySpecSummaryAdapter.vue'
import FlakyBadge from './FlakyBadge.vue'
import FlakySpecSummary from './FlakySpecSummary.vue'
import { getUrlWithParams } from '@packages/frontend-shared/src/utils/getUrlWithParams'
gql`
@@ -66,11 +67,15 @@ fragment FlakyInformationCloudSpec on RemoteFetchableCloudProjectSpecResult {
data {
... on CloudProjectSpec {
id
isConsideredFlaky(fromBranch: $fromBranch)
flakyStatus(fromBranch: $fromBranch, flakyRunsWindow: 50) {
isConsideredFlakyForRunIds(cloudRunIds: $runIds)
flakyStatusForRunIds(cloudRunIds: $runIds) {
__typename
... on CloudProjectSpecFlakyStatus {
dashboardUrl
severity
flakyRuns
flakyRunsWindow
lastFlaky
}
}
}
@@ -84,13 +89,12 @@ const props = defineProps<{
cloudSpecGql: FlakyInformationCloudSpecFragment | null | undefined
}>()
const isFlaky = computed(() => props.cloudSpecGql?.data?.__typename === 'CloudProjectSpec' && !!props.cloudSpecGql?.data?.isConsideredFlaky)
const cloudSpec = computed(() => props.cloudSpecGql?.data?.__typename === 'CloudProjectSpec' ? props.cloudSpecGql.data : null)
const isFlaky = computed(() => !!cloudSpec.value?.isConsideredFlakyForRunIds)
const flakyStatus = computed(() => cloudSpec.value?.flakyStatusForRunIds?.__typename === 'CloudProjectSpecFlakyStatus' ? cloudSpec.value?.flakyStatusForRunIds : null)
const cloudUrl = computed(() => {
const cloudSpec = props.cloudSpecGql?.data?.__typename === 'CloudProjectSpec' ? props.cloudSpecGql.data : null
const flakyStatus = cloudSpec?.flakyStatus?.__typename === 'CloudProjectSpecFlakyStatus' ? cloudSpec.flakyStatus : null
return getUrlWithParams({
url: flakyStatus?.dashboardUrl || '#',
url: flakyStatus.value?.dashboardUrl || '#',
params: {
utm_medium: 'Specs Flake Annotation Badge',
utm_campaign: 'Flaky',

View File

@@ -1,86 +0,0 @@
<template>
<FlakySpecSummary
:spec-name="specName"
:spec-extension="specExtension"
:severity="flakyStatus?.severity ?? 'NONE'"
:total-runs="flakyStatus?.flakyRunsWindow ?? 0"
:total-flaky-runs="flakyStatus?.flakyRuns ?? 0"
:runs-since-last-flake="flakyStatus?.lastFlaky ?? 0"
/>
</template>
<script setup lang="ts">
import { computed, onBeforeMount } from 'vue'
import FlakySpecSummary from './FlakySpecSummary.vue'
import { gql, useMutation, useQuery } from '@urql/vue'
import { FlakySpecSummaryQueryDocument, PurgeCloudSpecCacheDocument } from '../../generated/graphql'
gql`
fragment FlakySpecSummaryQueryData on Query {
cloudSpecByPath(projectSlug: $projectId, specPath: $specPath) {
__typename
... on CloudProjectSpec {
id
flakyStatus(fromBranch: $fromBranch, flakyRunsWindow: 50) {
__typename
... on CloudProjectSpecFlakyStatus {
severity
flakyRuns
flakyRunsWindow
lastFlaky
}
}
}
}
}
`
gql`
query FlakySpecSummaryQuery($projectId: String!, $specPath: String!, $fromBranch: String!) {
...FlakySpecSummaryQueryData
}
`
gql`
mutation PurgeCloudSpecCache ($projectSlug: String!, $specPaths: [String!]!) {
purgeCloudSpecByPathCache(projectSlug: $projectSlug, specPaths: $specPaths)
}
`
const props = defineProps<{
projectId: string
specName: string
specExtension: string
specPath: string
fromBranch: string
}>()
const variables = computed(() => {
return {
projectId: props.projectId,
specPath: props.specPath,
fromBranch: props.fromBranch,
}
})
const query = useQuery({ query: FlakySpecSummaryQueryDocument, variables, pause: true })
const purgeCloudSpecCacheMutation = useMutation(PurgeCloudSpecCacheDocument)
const flakyStatus = computed(() => {
if (query.data.value?.cloudSpecByPath?.__typename === 'CloudProjectSpec' &&
query.data.value?.cloudSpecByPath?.flakyStatus?.__typename === 'CloudProjectSpecFlakyStatus') {
return query.data.value.cloudSpecByPath.flakyStatus
}
return null
})
onBeforeMount(async () => {
// Ensure we have the latest flaky data - since we manually query for flaky data here the background polling
// won't auto-refetch it for us when stale data is detected based on lastProjectUpdate
await purgeCloudSpecCacheMutation.executeMutation({ projectSlug: props.projectId, specPaths: [props.specPath] })
await query.executeQuery({ requestPolicy: 'network-only' })
})
</script>

View File

@@ -32,6 +32,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
},
'env': {},
'execTimeout': 60000,
'experimentalCspAllowList': false,
'experimentalFetchPolyfill': false,
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
@@ -119,6 +120,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
},
'env': {},
'execTimeout': 60000,
'experimentalCspAllowList': false,
'experimentalFetchPolyfill': false,
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
@@ -202,6 +204,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key
'e2e',
'env',
'execTimeout',
'experimentalCspAllowList',
'experimentalFetchPolyfill',
'experimentalInteractiveRunEvents',
'experimentalRunAllSpecs',

View File

@@ -225,6 +225,31 @@ exports['config/src/validation .isStringOrFalse returns error message when value
'type': 'a string or false',
}
exports['not an array error message'] = {
'key': 'fakeKey',
'value': 'fakeValue',
'type': 'an array including any of these values: [true, false]',
}
exports['not a subset of error message'] = {
'key': 'fakeKey',
'value': [
null,
],
'type': 'an array including any of these values: ["fakeValue", "fakeValue1", "fakeValue2"]',
}
exports['not all in subset error message'] = {
'key': 'fakeKey',
'value': [
'fakeValue',
'fakeValue1',
'fakeValue2',
'fakeValue3',
],
'type': 'an array including any of these values: ["fakeValue", "fakeValue1", "fakeValue2"]',
}
exports['invalid lower bound'] = {
'key': 'test',
'value': -1,

View File

@@ -196,6 +196,12 @@ const driverConfigOptions: Array<DriverConfigOption> = [
defaultValue: 60000,
validation: validate.isNumber,
overrideLevel: 'any',
}, {
name: 'experimentalCspAllowList',
defaultValue: false,
validation: validate.validateAny(validate.isBoolean, validate.isArrayIncludingAny('script-src-elem', 'script-src', 'default-src', 'form-action', 'child-src', 'frame-src')),
overrideLevel: 'never',
requireRestartOnChange: 'server',
}, {
name: 'experimentalFetchPolyfill',
defaultValue: false,

View File

@@ -33,7 +33,7 @@ const _isFullyQualifiedUrl = (value: any): ErrResult | boolean => {
return _.isString(value) && /^https?\:\/\//.test(value)
}
const isArrayOfStrings = (value: any): ErrResult | boolean => {
const isStringArray = (value: any): ErrResult | boolean => {
return _.isArray(value) && _.every(value, _.isString)
}
@@ -41,6 +41,21 @@ const isFalse = (value: any): boolean => {
return value === false
}
type ValidationResult = ErrResult | boolean | string;
type ValidationFn = (key: string, value: any) => ValidationResult
export const validateAny = (...validations: ValidationFn[]): ValidationFn => {
return (key: string, value: any): ValidationResult => {
return validations.reduce((result: ValidationResult, validation: ValidationFn) => {
if (result === true) {
return result
}
return validation(key, value)
}, false)
}
}
/**
* Validates a single browser object.
* @returns {string|true} Returns `true` if the object is matching browser object schema. Returns an error message if it does not.
@@ -148,6 +163,29 @@ export const isOneOf = (...values: any[]): ((key: string, value: any) => ErrResu
}
}
/**
* Checks if given array value for a key includes only members of the provided values.
* @example
```
validate = v.isArrayIncludingAny("foo", "bar", "baz")
validate("example", ["foo"]) // true
validate("example", ["bar", "baz"]) // true
validate("example", ["foo", "else"]) // error message string
validate("example", ["foo", "bar", "baz", "else"]) // error message string
```
*/
export const isArrayIncludingAny = (...values: any[]): ((key: string, value: any) => ErrResult | true) => {
const validValues = values.map((a) => str(a)).join(', ')
return (key, value) => {
if (!Array.isArray(value) || !value.every((v) => values.includes(v))) {
return errMsg(key, value, `an array including any of these values: [${validValues}]`)
}
return true
}
}
/**
* Validates whether the supplied set of cert information is valid
* @returns {string|true} Returns `true` if the information set is valid. Returns an error message if it is not.
@@ -332,7 +370,7 @@ export function isFullyQualifiedUrl (key: string, value: any): ErrResult | true
}
export function isStringOrArrayOfStrings (key: string, value: any): ErrResult | true {
if (_.isString(value) || isArrayOfStrings(value)) {
if (_.isString(value) || isStringArray(value)) {
return true
}
@@ -340,7 +378,7 @@ export function isStringOrArrayOfStrings (key: string, value: any): ErrResult |
}
export function isNullOrArrayOfStrings (key: string, value: any): ErrResult | true {
if (_.isNull(value) || isArrayOfStrings(value)) {
if (_.isNull(value) || isStringArray(value)) {
return true
}

View File

@@ -838,6 +838,34 @@ describe('config/src/project/utils', () => {
})
})
it('experimentalCspAllowList=false', function () {
return this.defaults('experimentalCspAllowList', false)
})
it('experimentalCspAllowList=true', function () {
return this.defaults('experimentalCspAllowList', true, {
experimentalCspAllowList: true,
})
})
it('experimentalCspAllowList=[]', function () {
return this.defaults('experimentalCspAllowList', [], {
experimentalCspAllowList: [],
})
})
it('experimentalCspAllowList=default-src|script-src', function () {
return this.defaults('experimentalCspAllowList', ['default-src', 'script-src'], {
experimentalCspAllowList: ['default-src', 'script-src'],
})
})
it('experimentalCspAllowList=["default-src","script-src"]', function () {
return this.defaults('experimentalCspAllowList', ['default-src', 'script-src'], {
experimentalCspAllowList: ['default-src', 'script-src'],
})
})
it('resets numTestsKeptInMemory to 0 when runMode', function () {
return mergeDefaults({ projectRoot: '/foo/bar/', supportFile: false }, { isTextTerminal: true }, {}, this.getFilesByGlob)
.then((cfg) => {
@@ -1032,6 +1060,7 @@ describe('config/src/project/utils', () => {
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalSkipDomainInjection: { value: null, from: 'default' },
experimentalCspAllowList: { value: false, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },
@@ -1127,6 +1156,7 @@ describe('config/src/project/utils', () => {
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalSkipDomainInjection: { value: null, from: 'default' },
experimentalCspAllowList: { value: false, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalMemoryManagement: { value: false, from: 'default' },

View File

@@ -6,6 +6,39 @@ import * as validation from '../src/validation'
describe('config/src/validation', () => {
const mockKey = 'mockConfigKey'
describe('.validateAny', () => {
it('returns new validation function that accepts 2 arguments', () => {
const validate = validation.validateAny(() => true, () => false)
expect(validate).to.be.a.instanceof(Function)
expect(validate.length).to.eq(2)
})
it('returned validation function will return true when any validations pass', () => {
const value = Date.now()
const key = `key_${value}`
const validatePass1 = validation.validateAny((k, v) => `${value}`, (k, v) => true)
expect(validatePass1(key, value)).to.equal(true)
const validatePass2 = validation.validateAny((k, v) => true, (k, v) => `${value}`)
expect(validatePass2(key, value)).to.equal(true)
})
it('returned validation function will return last failure result when all validations fail', () => {
const value = Date.now()
const key = `key_${value}`
const validateFail1 = validation.validateAny((k, v) => `${value}`, (k, v) => false)
expect(validateFail1(key, value)).to.equal(false)
const validateFail2 = validation.validateAny((k, v) => false, (k, v) => `${value}`)
expect(validateFail2(key, value)).to.equal(`${value}`)
})
})
describe('.isValidClientCertificatesSet', () => {
it('returns error message for certs not passed as an array array', () => {
const result = validation.isValidRetriesConfig(mockKey, '1')
@@ -389,6 +422,54 @@ describe('config/src/validation', () => {
})
})
describe('.isArrayIncludingAny', () => {
it('returns new validation function that accepts 2 arguments', () => {
const validate = validation.isArrayIncludingAny(true, false)
expect(validate).to.be.a.instanceof(Function)
expect(validate.length).to.eq(2)
})
it('returned validation function will return true when value is a subset of the provided values', () => {
const value = 'fakeValue'
const key = 'fakeKey'
const validatePass1 = validation.isArrayIncludingAny(true, false)
expect(validatePass1(key, [false])).to.equal(true)
const validatePass2 = validation.isArrayIncludingAny(value, value + 1, value + 2)
expect(validatePass2(key, [value])).to.equal(true)
})
it('returned validation function will fail if values is not an array', () => {
const value = 'fakeValue'
const key = 'fakeKey'
const validateFail = validation.isArrayIncludingAny(true, false)
let msg = validateFail(key, value)
expect(msg).to.not.be.true
snapshot('not an array error message', msg)
})
it('returned validation function will fail if any values are not present in the provided values', () => {
const value = 'fakeValue'
const key = 'fakeKey'
const validateFail = validation.isArrayIncludingAny(value, value + 1, value + 2)
let msg = validateFail(key, [null])
expect(msg).to.not.be.true
snapshot('not a subset of error message', msg)
msg = validateFail(key, [value, value + 1, value + 2, value + 3])
expect(msg).to.not.be.true
snapshot('not all in subset error message', msg)
})
})
describe('.isValidCrfOrBoolean', () => {
it('validates booleans', () => {
const validate = validation.isValidCrfOrBoolean

View File

@@ -44,7 +44,6 @@ import { ErrorDataSource } from './sources/ErrorDataSource'
import { GraphQLDataSource } from './sources/GraphQLDataSource'
import { RemoteRequestDataSource } from './sources/RemoteRequestDataSource'
import { resetIssuedWarnings } from '@packages/config'
import { RemotePollingDataSource } from './sources/RemotePollingDataSource'
const IS_DEV_ENV = process.env.CYPRESS_INTERNAL_ENV !== 'production'
@@ -220,11 +219,6 @@ export class DataContext {
return new ProjectDataSource(this)
}
@cached
get remotePolling () {
return new RemotePollingDataSource(this)
}
@cached
get relevantRuns () {
return new RelevantRunsDataSource(this)

View File

@@ -467,6 +467,7 @@ export class GitDataSource {
}
__setGitHashesForTesting (hashes: string[]) {
debug('Setting git hashes for testing', hashes)
this.#gitHashes = hashes
}
}

View File

@@ -91,7 +91,7 @@ export class RelevantRunSpecsDataSource {
debug('subscriptions', subscriptions)
const runIds = uniq(compact(subscriptions?.map((sub) => sub.meta?.runId)))
debug('Polling for specs for runs: %o - runIds: %o', runIds)
debug('Polling for specs for runs: %o', runIds)
const query = this.createQuery(compact(subscriptions.map((sub) => sub.meta?.info)))
@@ -104,6 +104,10 @@ export class RelevantRunSpecsDataSource {
debug(`Run data is `, runs)
runs.forEach(async (run) => {
if (!run) {
return
}
const cachedRun = this.#cached.get(run.id)
if (!cachedRun || !isEqual(run, cachedRun)) {

View File

@@ -1,7 +1,7 @@
import { gql } from '@urql/core'
import { print } from 'graphql'
import debugLib from 'debug'
import { isEqual, takeWhile } from 'lodash'
import { isEqual, take, takeWhile } from 'lodash'
import type { DataContext } from '../DataContext'
import type { Query, RelevantRun, RelevantRunInfo, RelevantRunLocationEnum } from '../gen/graphcache-config.gen'
@@ -37,7 +37,7 @@ const RELEVANT_RUN_OPERATION_DOC = gql`
const RELEVANT_RUN_UPDATE_OPERATION = print(RELEVANT_RUN_OPERATION_DOC)
export const RUNS_EMPTY_RETURN: RelevantRun = { commitsAhead: -1, all: [] }
export const RUNS_EMPTY_RETURN: RelevantRun = { commitsAhead: -1, all: [], latest: [] }
/**
* DataSource to encapsulate querying Cypress Cloud for runs that match a list of local Git commit shas
@@ -118,6 +118,7 @@ export class RelevantRunsDataSource {
return run != null && !!run.runNumber && !!run.status && !!run.commitInfo?.sha
}).map((run) => {
return {
runId: run.id,
runNumber: run.runNumber!,
status: run.status!,
sha: run.commitInfo?.sha!,
@@ -140,8 +141,9 @@ export class RelevantRunsDataSource {
if (run) {
//filter relevant runs in case moving causes the previously selected run to no longer be relevant
const relevantRuns = this.#takeRelevantRuns(this.#cached.all)
const latestRuns = this.#cached.latest
await this.#emitRelevantRunsIfChanged({ relevantRuns, selectedRun: run, shas })
await this.#emitRelevantRunsIfChanged({ relevantRuns, selectedRun: run, shas, latestRuns })
}
}
@@ -181,6 +183,8 @@ export class RelevantRunsDataSource {
const relevantRuns: RelevantRunInfo[] = this.#takeRelevantRuns(runs)
const latestRuns: RelevantRunInfo[] = this.#takeLatestRuns(runs)
// If there is a selected run that is no longer considered relevant,
// make sure to still add it to the list of runs
const selectedRunNumber = selectedRun?.runNumber
@@ -196,7 +200,7 @@ export class RelevantRunsDataSource {
}
}
await this.#emitRelevantRunsIfChanged({ relevantRuns, selectedRun, shas })
await this.#emitRelevantRunsIfChanged({ relevantRuns, selectedRun, shas, latestRuns })
}
#takeRelevantRuns (runs: RelevantRunInfo[]) {
@@ -210,20 +214,30 @@ export class RelevantRunsDataSource {
return run.status === 'RUNNING' || run.sha === firstShaWithCompletedRun
})
debug('runs after take', relevantRuns)
debug('relevant runs after take', relevantRuns)
return relevantRuns
}
async #emitRelevantRunsIfChanged ({ relevantRuns, selectedRun, shas }: {
#takeLatestRuns (runs: RelevantRunInfo[]) {
const latestRuns = take(runs, 10)
debug('latest runs after take', latestRuns)
return latestRuns
}
async #emitRelevantRunsIfChanged ({ relevantRuns, selectedRun, shas, latestRuns }: {
relevantRuns: RelevantRunInfo[]
selectedRun: RelevantRunInfo | undefined
shas: string[]
latestRuns: RelevantRunInfo[]
}) {
const commitsAhead = selectedRun?.sha ? shas.indexOf(selectedRun.sha) : -1
const toCache: RelevantRun = {
all: relevantRuns,
latest: latestRuns,
commitsAhead,
selectedRunNumber: selectedRun?.runNumber,
}

View File

@@ -1,101 +0,0 @@
import { gql } from '@urql/core'
import { print } from 'graphql'
import debugLib from 'debug'
import type { DataContext } from '../DataContext'
import type { Query } from '../gen/graphcache-config.gen'
const debug = debugLib('cypress:data-context:sources:RemotePollingDataSource')
const LATEST_RUN_UPDATE_OPERATION_DOC = gql`
query RemotePollingDataSource_latestRunUpdateSpecData(
$commitBranch: String!
$projectSlug: String!
# sinceDateTime: DateTime
) {
cloudLatestRunUpdateSpecData(commitBranch: $commitBranch, projectSlug: $projectSlug) {
mostRecentUpdate
pollingInterval
}
}
`
const LATEST_RUN_UPDATE_OPERATION = print(LATEST_RUN_UPDATE_OPERATION_DOC)
export class RemotePollingDataSource {
#subscribedCount = 0
#specPolling?: NodeJS.Timeout
constructor (private ctx: DataContext) {}
#startPollingForSpecs (branch: string, projectSlug: string) {
// when the page refreshes, a previously started subscription may be running
// this will reset it and start a new one
if (this.#specPolling) {
clearTimeout(this.#specPolling)
}
debug(`Sending initial request for startPollingForSpecs`)
// Send the spec polling request
this.#sendSpecPollingRequest(branch, projectSlug).catch((e) => {
debug(`Error executing specPollingRequest %o`, e)
})
}
#stopPolling () {
if (this.#specPolling) {
clearTimeout(this.#specPolling)
this.#specPolling = undefined
}
}
async #sendSpecPollingRequest (commitBranch: string, projectSlug: string) {
const result = await this.ctx.cloud.executeRemoteGraphQL<Pick<Query, 'cloudLatestRunUpdateSpecData'>>({
fieldName: 'cloudLatestRunUpdateSpecData',
operationDoc: LATEST_RUN_UPDATE_OPERATION_DOC,
operation: LATEST_RUN_UPDATE_OPERATION,
operationVariables: {
commitBranch,
projectSlug,
},
requestPolicy: 'network-only', // we never want to hit local cache for this request
})
debug(`%s Response for startPollingForSpecs %o`, new Date().toISOString(), result)
const secondsToPollNext = (result.data?.cloudLatestRunUpdateSpecData?.pollingInterval ?? 30)
const mostRecentUpdate = result.data?.cloudLatestRunUpdateSpecData?.mostRecentUpdate ?? null
this.ctx.emitter.specPollingUpdate(mostRecentUpdate)
this.#specPolling = setTimeout(async () => {
await this.#sendSpecPollingRequest(commitBranch, projectSlug)
}, secondsToPollNext * 1000)
return result
}
subscribeAndPoll (branch?: string | null, projectSlug?: string | null) {
if (!branch || !projectSlug) {
return this.ctx.emitter.subscribeTo('noopChange', { sendInitial: false })
}
debug('Subscribing, subscribed count %d', this.#subscribedCount)
if (this.#subscribedCount === 0) {
debug('Starting polling')
this.#startPollingForSpecs(branch, projectSlug)
}
this.#subscribedCount++
return this.ctx.emitter.subscribeTo('specPollingUpdate', {
sendInitial: false,
onUnsubscribe: () => {
debug('Unsubscribing, subscribed count %d', this.#subscribedCount)
this.#subscribedCount--
if (this.#subscribedCount === 0) {
this.#stopPolling()
}
},
})
}
}

View File

@@ -13,7 +13,6 @@ export * from './MigrationDataSource'
export * from './ProjectDataSource'
export * from './RelevantRunSpecsDataSource'
export * from './RelevantRunsDataSource'
export * from './RemotePollingDataSource'
export * from './RemoteRequestDataSource'
export * from './UtilDataSource'
export * from './VersionsDataSource'

View File

@@ -17,8 +17,8 @@ type TestProject = typeof _PROJECTS[number]
function formatRun (project: TestProject, index: number) {
const run = project.data.cloudProjectBySlug.runsByCommitShas?.[index]
return (({ status, runNumber, commitInfo }) => {
return { status, runNumber, sha: commitInfo.sha }
return (({ status, runNumber, commitInfo, id }) => {
return { status, runNumber, sha: commitInfo.sha, runId: id }
})(run)
}
@@ -160,6 +160,7 @@ describe('RelevantRunsDataSource', () => {
expect(subValues[0], 'should emit first result of running').to.eql({
all: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
commitsAhead: 0,
latest: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
selectedRunNumber: 1,
})
@@ -168,12 +169,20 @@ describe('RelevantRunsDataSource', () => {
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 1),
],
latest: [
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 1),
],
commitsAhead: 1,
selectedRunNumber: 1,
})
expect(subValues[2], 'should emit selected run after moving').to.eql({
all: [formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0)],
latest: [
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 1),
],
commitsAhead: 0,
selectedRunNumber: 4,
})
@@ -213,12 +222,17 @@ describe('RelevantRunsDataSource', () => {
expect(subValues[0], 'should emit first result of running').to.eql({
all: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
latest: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
commitsAhead: 0,
selectedRunNumber: 1,
})
expect(subValues[1], 'should emit newer completed run on different sha').to.eql({
all: [formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0)],
latest: [
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 1),
],
commitsAhead: 0,
selectedRunNumber: 4,
})

View File

@@ -1,2 +1,3 @@
cypress/videos
cypress/screenshots
cypress/downloads

View File

@@ -12,14 +12,18 @@ describe('src/cy/commands/exec', () => {
cy.stub(Cypress, 'backend').log(false).callThrough()
})
it('triggers \'exec\' with the right options', () => {
it('sends privileged exec to backend with the right options', () => {
Cypress.backend.resolves(okResponse)
cy.exec('ls').then(() => {
expect(Cypress.backend).to.be.calledWith('exec', {
cmd: 'ls',
timeout: 2500,
env: {},
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'exec',
userArgs: ['ls'],
options: {
cmd: 'ls',
timeout: 2500,
env: {},
},
})
})
})
@@ -28,17 +32,19 @@ describe('src/cy/commands/exec', () => {
Cypress.backend.resolves(okResponse)
cy.exec('ls', { env: { FOO: 'foo' } }).then(() => {
expect(Cypress.backend).to.be.calledWith('exec', {
cmd: 'ls',
timeout: 2500,
env: {
FOO: 'foo',
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'exec',
userArgs: ['ls', { env: { FOO: 'foo' } }],
options: {
cmd: 'ls',
timeout: 2500,
env: { FOO: 'foo' },
},
})
})
})
it('really works', () => {
it('works e2e', () => {
// output is trimmed
cy.exec('echo foo', { timeout: 20000 }).its('stdout').should('eq', 'foo')
})
@@ -188,7 +194,7 @@ describe('src/cy/commands/exec', () => {
})
it('throws when the execution errors', function (done) {
Cypress.backend.withArgs('exec').rejects(new Error('exec failed'))
Cypress.backend.withArgs('run:privileged').rejects(new Error('exec failed'))
cy.on('fail', (err) => {
const { lastLog } = this
@@ -207,7 +213,7 @@ describe('src/cy/commands/exec', () => {
})
it('throws after timing out', function (done) {
Cypress.backend.withArgs('exec').resolves(Promise.delay(250))
Cypress.backend.withArgs('run:privileged').resolves(Promise.delay(250))
cy.on('fail', (err) => {
const { lastLog } = this
@@ -225,7 +231,7 @@ describe('src/cy/commands/exec', () => {
})
it('logs once on error', function (done) {
Cypress.backend.withArgs('exec').rejects(new Error('exec failed'))
Cypress.backend.withArgs('run:privileged').rejects(new Error('exec failed'))
cy.on('fail', (err) => {
const { lastLog } = this
@@ -245,7 +251,7 @@ describe('src/cy/commands/exec', () => {
err.timedOut = true
Cypress.backend.withArgs('exec').rejects(err)
Cypress.backend.withArgs('run:privileged').rejects(err)
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.exec(\'sleep 2\')` timed out after waiting `100ms`.')

View File

@@ -22,48 +22,66 @@ describe('src/cy/commands/files', () => {
cy.readFile('does-not-exist').should('be.null')
})
it('triggers \'read:file\' with the right options', () => {
Cypress.backend.withArgs('read:file').resolves(okResponse)
it('sends privileged readFile to backend with the right options', () => {
Cypress.backend.resolves(okResponse)
cy.readFile('foo.json').then(() => {
expect(Cypress.backend).to.be.calledWith(
'read:file',
'foo.json',
{ encoding: 'utf8' },
'run:privileged',
{
commandName: 'readFile',
userArgs: ['foo.json'],
options: {
file: 'foo.json',
encoding: 'utf8',
},
},
)
})
})
it('can take encoding as second argument', () => {
Cypress.backend.withArgs('read:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.readFile('foo.json', 'ascii').then(() => {
expect(Cypress.backend).to.be.calledWith(
'read:file',
'foo.json',
{ encoding: 'ascii' },
'run:privileged',
{
commandName: 'readFile',
userArgs: ['foo.json', 'ascii'],
options: {
file: 'foo.json',
encoding: 'ascii',
},
},
)
})
})
// https://github.com/cypress-io/cypress/issues/1558
it('passes explicit null encoding through to server and decodes response', () => {
Cypress.backend.withArgs('read:file').resolves({
Cypress.backend.resolves({
contents: Buffer.from('\n'),
filePath: '/path/to/foo.json',
})
cy.readFile('foo.json', null).then(() => {
expect(Cypress.backend).to.be.calledWith(
'read:file',
'foo.json',
{ encoding: null },
'run:privileged',
{
commandName: 'readFile',
userArgs: ['foo.json', null],
options: {
file: 'foo.json',
encoding: null,
},
},
)
}).should('eql', Buffer.from('\n'))
})
it('sets the contents as the subject', () => {
Cypress.backend.withArgs('read:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.readFile('foo.json').then((subject) => {
expect(subject).to.equal('contents')
@@ -81,7 +99,7 @@ describe('src/cy/commands/files', () => {
retries += 1
})
Cypress.backend.withArgs('read:file')
Cypress.backend.withArgs('run:privileged')
.onFirstCall()
.rejects(err)
.onSecondCall()
@@ -99,7 +117,7 @@ describe('src/cy/commands/files', () => {
retries += 1
})
Cypress.backend.withArgs('read:file')
Cypress.backend.withArgs('run:privileged')
.onFirstCall()
.resolves({
contents: 'foobarbaz',
@@ -129,7 +147,7 @@ describe('src/cy/commands/files', () => {
})
it('can turn off logging', () => {
Cypress.backend.withArgs('read:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.readFile('foo.json', { log: false }).then(function () {
const logs = _.filter(this.logs, (log) => {
@@ -141,7 +159,7 @@ describe('src/cy/commands/files', () => {
})
it('logs immediately before resolving', function () {
Cypress.backend.withArgs('read:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'readFile') {
@@ -236,7 +254,7 @@ describe('src/cy/commands/files', () => {
err.code = 'EISDIR'
err.filePath = '/path/to/foo'
Cypress.backend.withArgs('read:file').rejects(err)
Cypress.backend.withArgs('run:privileged').rejects(err)
cy.on('fail', (err) => {
const { fileLog } = this
@@ -268,7 +286,7 @@ describe('src/cy/commands/files', () => {
err.code = 'ENOENT'
err.filePath = '/path/to/foo.json'
Cypress.backend.withArgs('read:file').rejects(err)
Cypress.backend.withArgs('run:privileged').rejects(err)
cy.on('fail', (err) => {
const { fileLog } = this
@@ -297,7 +315,7 @@ describe('src/cy/commands/files', () => {
err.code = 'ENOENT'
err.filePath = '/path/to/foo.json'
Cypress.backend.withArgs('read:file').rejects(err)
Cypress.backend.withArgs('run:privileged').rejects(err)
let hasRetried = false
cy.on('command:retry', () => {
@@ -326,7 +344,7 @@ describe('src/cy/commands/files', () => {
})
it('throws a specific error when file exists when it shouldn\'t', function (done) {
Cypress.backend.withArgs('read:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.on('fail', (err) => {
const { fileLog, logs } = this
@@ -353,7 +371,7 @@ describe('src/cy/commands/files', () => {
})
it('passes through assertion error when not about existence', function (done) {
Cypress.backend.withArgs('read:file').resolves({
Cypress.backend.resolves({
contents: 'foo',
})
@@ -376,7 +394,7 @@ describe('src/cy/commands/files', () => {
})
it('throws when the read timeout expires', function (done) {
Cypress.backend.withArgs('read:file').callsFake(() => {
Cypress.backend.withArgs('run:privileged').callsFake(() => {
return new Cypress.Promise(() => { /* Broken promise for timeout */ })
})
@@ -400,7 +418,7 @@ describe('src/cy/commands/files', () => {
it('uses defaultCommandTimeout config value if option not provided', {
defaultCommandTimeout: 42,
}, function (done) {
Cypress.backend.withArgs('read:file').callsFake(() => {
Cypress.backend.withArgs('run:privileged').callsFake(() => {
return new Cypress.Promise(() => { /* Broken promise for timeout */ })
})
@@ -424,33 +442,41 @@ describe('src/cy/commands/files', () => {
})
describe('#writeFile', () => {
it('triggers \'write:file\' with the right options', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
it('sends privileged writeFile to backend with the right options', () => {
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents').then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.txt',
'contents',
'run:privileged',
{
encoding: 'utf8',
flag: 'w',
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents'],
options: {
fileName: 'foo.txt',
contents: 'contents',
encoding: 'utf8',
flag: 'w',
},
},
)
})
})
it('can take encoding as third argument', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents', 'ascii').then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.txt',
'contents',
'run:privileged',
{
encoding: 'ascii',
flag: 'w',
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents', 'ascii'],
options: {
fileName: 'foo.txt',
contents: 'contents',
encoding: 'ascii',
flag: 'w',
},
},
)
})
@@ -458,39 +484,49 @@ describe('src/cy/commands/files', () => {
// https://github.com/cypress-io/cypress/issues/1558
it('explicit null encoding is sent to server as Buffer', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', Buffer.from([0, 0, 54, 255]), null).then(() => {
const buffer = Buffer.from([0, 0, 54, 255])
cy.writeFile('foo.txt', buffer, null).then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.txt',
Buffer.from([0, 0, 54, 255]),
'run:privileged',
{
encoding: null,
flag: 'w',
commandName: 'writeFile',
userArgs: ['foo.txt', buffer, null],
options: {
fileName: 'foo.txt',
contents: buffer,
encoding: null,
flag: 'w',
},
},
)
})
})
it('can take encoding as part of options', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents', { encoding: 'ascii' }).then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.txt',
'contents',
'run:privileged',
{
encoding: 'ascii',
flag: 'w',
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents', { encoding: 'ascii' }],
options: {
fileName: 'foo.txt',
contents: 'contents',
encoding: 'ascii',
flag: 'w',
},
},
)
})
})
it('yields null', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents').then((subject) => {
expect(subject).to.eq(null)
@@ -498,19 +534,19 @@ describe('src/cy/commands/files', () => {
})
it('can write a string', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents')
})
it('can write an array as json', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.json', [])
})
it('can write an object as json', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.json', {})
})
@@ -525,16 +561,20 @@ describe('src/cy/commands/files', () => {
describe('.flag', () => {
it('sends a flag if specified', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents', { flag: 'a+' }).then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.txt',
'contents',
'run:privileged',
{
encoding: 'utf8',
flag: 'a+',
commandName: 'writeFile',
userArgs: ['foo.txt', 'contents', { flag: 'a+' }],
options: {
fileName: 'foo.txt',
contents: 'contents',
encoding: 'utf8',
flag: 'a+',
},
},
)
})
@@ -562,7 +602,7 @@ describe('src/cy/commands/files', () => {
})
it('can turn off logging', () => {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.writeFile('foo.txt', 'contents', { log: false }).then(function () {
const logs = _.filter(this.logs, (log) => {
@@ -574,7 +614,7 @@ describe('src/cy/commands/files', () => {
})
it('logs immediately before resolving', function () {
Cypress.backend.withArgs('write:file').resolves(okResponse)
Cypress.backend.resolves(okResponse)
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'writeFile') {
@@ -677,7 +717,7 @@ describe('src/cy/commands/files', () => {
err.code = 'WHOKNOWS'
err.filePath = '/path/to/foo.txt'
Cypress.backend.withArgs('write:file').rejects(err)
Cypress.backend.withArgs('run:privileged').rejects(err)
cy.on('fail', (err) => {
const { lastLog } = this
@@ -703,7 +743,7 @@ describe('src/cy/commands/files', () => {
})
it('throws when the write timeout expires', function (done) {
Cypress.backend.withArgs('write:file').callsFake(() => {
Cypress.backend.withArgs('run:privileged').callsFake(() => {
return new Cypress.Promise(() => {})
})
@@ -728,7 +768,7 @@ describe('src/cy/commands/files', () => {
it('uses defaultCommandTimeout config value if option not provided', {
defaultCommandTimeout: 42,
}, function (done) {
Cypress.backend.withArgs('write:file').callsFake(() => {
Cypress.backend.withArgs('run:privileged').callsFake(() => {
return new Cypress.Promise(() => { /* Broken promise for timeout */ })
})

View File

@@ -9,14 +9,18 @@ describe('src/cy/commands/task', () => {
cy.stub(Cypress, 'backend').log(false).callThrough()
})
it('calls Cypress.backend(\'task\') with the right options', () => {
it('sends privileged task to backend with the right options', () => {
Cypress.backend.resolves(null)
cy.task('foo').then(() => {
expect(Cypress.backend).to.be.calledWith('task', {
task: 'foo',
timeout: 2500,
arg: undefined,
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'task',
userArgs: ['foo'],
options: {
task: 'foo',
timeout: 2500,
arg: undefined,
},
})
})
})
@@ -25,11 +29,13 @@ describe('src/cy/commands/task', () => {
Cypress.backend.resolves(null)
cy.task('foo', { foo: 'foo' }).then(() => {
expect(Cypress.backend).to.be.calledWith('task', {
task: 'foo',
timeout: 2500,
arg: {
foo: 'foo',
expect(Cypress.backend).to.be.calledWith('run:privileged', {
commandName: 'task',
userArgs: ['foo', { foo: 'foo' }],
options: {
task: 'foo',
timeout: 2500,
arg: { foo: 'foo' },
},
})
})
@@ -184,7 +190,7 @@ describe('src/cy/commands/task', () => {
})
it('throws when the task errors', function (done) {
Cypress.backend.withArgs('task').rejects(new Error('task failed'))
Cypress.backend.withArgs('run:privileged').rejects(new Error('task failed'))
cy.on('fail', (err) => {
const { lastLog } = this
@@ -219,7 +225,7 @@ describe('src/cy/commands/task', () => {
})
it('throws after timing out', function (done) {
Cypress.backend.withArgs('task').resolves(Promise.delay(250))
Cypress.backend.withArgs('run:privileged').resolves(Promise.delay(250))
cy.on('fail', (err) => {
const { lastLog } = this
@@ -237,7 +243,7 @@ describe('src/cy/commands/task', () => {
})
it('logs once on error', function (done) {
Cypress.backend.withArgs('task').rejects(new Error('task failed'))
Cypress.backend.withArgs('run:privileged').rejects(new Error('task failed'))
cy.on('fail', (err) => {
const { lastLog } = this
@@ -257,7 +263,7 @@ describe('src/cy/commands/task', () => {
err.timedOut = true
Cypress.backend.withArgs('task').rejects(err)
Cypress.backend.withArgs('run:privileged').rejects(err)
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.task(\'wait\')` timed out after waiting `100ms`.')

View File

@@ -22,8 +22,13 @@ describe('src/cypress/script_utils', () => {
cy.stub($sourceMapUtils, 'initializeSourceMapConsumer').resolves()
})
it('fetches each script', () => {
return $scriptUtils.runScripts(scriptWindow, scripts)
it('fetches each script in non-webkit browsers', () => {
return $scriptUtils.runScripts({
browser: { family: 'chromium' },
scripts,
specWindow: scriptWindow,
testingType: 'e2e',
})
.then(() => {
expect($networkUtils.fetch).to.be.calledTwice
expect($networkUtils.fetch).to.be.calledWith(scripts[0].relativeUrl)
@@ -31,8 +36,62 @@ describe('src/cypress/script_utils', () => {
})
})
it('appends each script in e2e webkit', async () => {
const foundScript = {
after: cy.stub(),
}
const createdScript1 = {
addEventListener: cy.stub(),
}
const createdScript2 = {
addEventListener: cy.stub(),
}
const doc = {
querySelector: cy.stub().returns(foundScript),
createElement: cy.stub(),
}
doc.createElement.onCall(0).returns(createdScript1)
doc.createElement.onCall(1).returns(createdScript2)
scriptWindow.document = doc
const runScripts = $scriptUtils.runScripts({
scripts,
specWindow: scriptWindow,
browser: { family: 'webkit' },
testingType: 'e2e',
})
// each script is appended and run before the next
await Promise.delay(1) // wait a tick due to promise
expect(createdScript1.addEventListener).to.be.calledWith('load')
createdScript1.addEventListener.lastCall.args[1]()
await Promise.delay(1) // wait a tick due to promise
expect(createdScript2.addEventListener).to.be.calledWith('load')
createdScript2.addEventListener.lastCall.args[1]()
await runScripts
// sets script src
expect(createdScript1.src).to.equal(scripts[0].relativeUrl)
expect(createdScript2.src).to.equal(scripts[1].relativeUrl)
// appends scripts
expect(foundScript.after).to.be.calledTwice
expect(foundScript.after).to.be.calledWith(createdScript1)
expect(foundScript.after).to.be.calledWith(createdScript2)
})
it('extracts the source map from each script', () => {
return $scriptUtils.runScripts(scriptWindow, scripts)
return $scriptUtils.runScripts({
browser: { family: 'chromium' },
scripts,
specWindow: scriptWindow,
testingType: 'e2e',
})
.then(() => {
expect($sourceMapUtils.extractSourceMap).to.be.calledTwice
expect($sourceMapUtils.extractSourceMap).to.be.calledWith('the script contents')
@@ -41,7 +100,12 @@ describe('src/cypress/script_utils', () => {
})
it('evals each script', () => {
return $scriptUtils.runScripts(scriptWindow, scripts)
return $scriptUtils.runScripts({
browser: { family: 'chromium' },
scripts,
specWindow: scriptWindow,
testingType: 'e2e',
})
.then(() => {
expect(scriptWindow.eval).to.be.calledTwice
expect(scriptWindow.eval).to.be.calledWith('the script contents\n//# sourceURL=http://localhost:3500cypress/integration/script1.js')
@@ -53,7 +117,12 @@ describe('src/cypress/script_utils', () => {
context('#runPromises', () => {
it('handles promises and doesnt try to fetch + eval manually', async () => {
const scriptsAsPromises = [() => Promise.resolve(), () => Promise.resolve()]
const result = await $scriptUtils.runScripts({}, scriptsAsPromises)
const result = await $scriptUtils.runScripts({
browser: { family: 'chromium' },
scripts: scriptsAsPromises,
specWindow: {},
testingType: 'e2e',
})
expect(result).to.have.length(scriptsAsPromises.length)
})

View File

@@ -0,0 +1,30 @@
describe('csp-headers', () => {
it('content-security-policy headers are always stripped', () => {
const route = '/fixtures/empty.html'
cy.intercept(route, (req) => {
req.continue((res) => {
res.headers['content-security-policy'] = `script-src http://not-here.net;`
})
})
cy.visit(route)
.wait(1000)
// Next verify that inline scripts are allowed, because if they aren't, the CSP header is not getting stripped
const inlineId = `__${Math.random()}`
cy.window().then((win) => {
expect(() => {
return win.eval(`
var script = document.createElement('script');
script.textContent = "window['${inlineId}'] = '${inlineId}'";
document.head.appendChild(script);
`)
}).not.to.throw() // CSP should be stripped, so this should not throw
// Inline script should have created the var
expect(win[`${inlineId}`]).to.equal(`${inlineId}`, 'CSP Headers are being stripped')
})
})
})

View File

@@ -35,12 +35,16 @@ context('cy.origin files', { browser: '!webkit' }, () => {
cy.writeFile('foo.json', contents).then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.json',
contents,
'run:privileged',
{
encoding: 'utf8',
flag: 'w',
commandName: 'writeFile',
userArgs: ['foo.json', contents],
options: {
fileName: 'foo.json',
contents,
encoding: 'utf8',
flag: 'w',
},
},
)
})

View File

@@ -208,7 +208,7 @@ context('cy.origin misc', { browser: '!webkit' }, () => {
it('verifies number of cy commands', () => {
// remove custom commands we added for our own testing
const customCommands = ['getAll', 'shouldWithTimeout', 'originLoadUtils']
const customCommands = ['getAll', 'shouldWithTimeout', 'originLoadUtils', 'runSupportFileCustomPrivilegedCommands']
// @ts-ignore
const actualCommands = Cypress._.pullAll([...Object.keys(cy.commandFns), ...Object.keys(cy.queryFns)], customCommands)
const expectedCommands = [

View File

@@ -74,7 +74,7 @@ context('cy.origin waiting', { browser: '!webkit' }, () => {
cy.intercept('/foo', (req) => {
// delay the response to ensure the wait will wait for response
req.reply({
delay: 100,
delay: 200,
body: response,
})
}).as('foo')

View File

@@ -7,7 +7,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a localhost domain name', () => {
cy.origin('localhost', () => undefined)
cy.then(() => {
const expectedSrc = `https://localhost/__cypress/spec-bridge-iframes`
const expectedSrc = `https://localhost/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://localhost') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -17,7 +17,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on an ip address', () => {
cy.origin('127.0.0.1', () => undefined)
cy.then(() => {
const expectedSrc = `https://127.0.0.1/__cypress/spec-bridge-iframes`
const expectedSrc = `https://127.0.0.1/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://127.0.0.1') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -29,7 +29,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it.skip('succeeds on an ipv6 address', () => {
cy.origin('0000:0000:0000:0000:0000:0000:0000:0001', () => undefined)
cy.then(() => {
const expectedSrc = `https://[::1]/__cypress/spec-bridge-iframes`
const expectedSrc = `https://[::1]/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://[::1]') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -39,7 +39,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a unicode domain', () => {
cy.origin('はじめよう.みんな', () => undefined)
cy.then(() => {
const expectedSrc = `https://xn--p8j9a0d9c9a.xn--q9jyb4c/__cypress/spec-bridge-iframes`
const expectedSrc = `https://xn--p8j9a0d9c9a.xn--q9jyb4c/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://xn--p8j9a0d9c9a.xn--q9jyb4c') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -49,7 +49,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a complete origin', () => {
cy.origin('http://foobar1.com:3500', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar1.com:3500/__cypress/spec-bridge-iframes`
const expectedSrc = `http://foobar1.com:3500/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar1.com:3500') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -59,7 +59,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a complete origin using https', () => {
cy.origin('https://www.foobar2.com:3500', () => undefined)
cy.then(() => {
const expectedSrc = `https://www.foobar2.com:3500/__cypress/spec-bridge-iframes`
const expectedSrc = `https://www.foobar2.com:3500/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://www.foobar2.com:3500') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -69,7 +69,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a hostname and port', () => {
cy.origin('foobar3.com:3500', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar3.com:3500/__cypress/spec-bridge-iframes`
const expectedSrc = `https://foobar3.com:3500/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar3.com:3500') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -79,7 +79,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a protocol and hostname', () => {
cy.origin('http://foobar4.com', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar4.com/__cypress/spec-bridge-iframes`
const expectedSrc = `http://foobar4.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar4.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -89,7 +89,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a subdomain', () => {
cy.origin('app.foobar5.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://app.foobar5.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://app.foobar5.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://app.foobar5.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -99,7 +99,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds when only domain is passed', () => {
cy.origin('foobar6.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar6.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://foobar6.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar6.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -109,7 +109,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a url with path', () => {
cy.origin('http://www.foobar7.com/login', () => undefined)
cy.then(() => {
const expectedSrc = `http://www.foobar7.com/__cypress/spec-bridge-iframes`
const expectedSrc = `http://www.foobar7.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://www.foobar7.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -119,7 +119,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a url with a hash', () => {
cy.origin('http://www.foobar8.com/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `http://www.foobar8.com/__cypress/spec-bridge-iframes`
const expectedSrc = `http://www.foobar8.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://www.foobar8.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -129,7 +129,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a url with a path and hash', () => {
cy.origin('http://www.foobar9.com/login/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `http://www.foobar9.com/__cypress/spec-bridge-iframes`
const expectedSrc = `http://www.foobar9.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://www.foobar9.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -139,7 +139,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a domain with path', () => {
cy.origin('foobar10.com/login', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar10.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://foobar10.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar10.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -149,7 +149,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a domain with a hash', () => {
cy.origin('foobar11.com/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar11.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://foobar11.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar11.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -159,7 +159,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a domain with a path and hash', () => {
cy.origin('foobar12.com/login/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar12.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://foobar12.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar12.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -169,7 +169,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a public suffix with a subdomain', () => {
cy.origin('app.foobar.herokuapp.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://app.foobar.herokuapp.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://app.foobar.herokuapp.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://app.foobar.herokuapp.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -179,7 +179,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
it('succeeds on a machine name', () => {
cy.origin('machine-name', () => undefined)
cy.then(() => {
const expectedSrc = `https://machine-name/__cypress/spec-bridge-iframes`
const expectedSrc = `https://machine-name/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://machine-name') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -356,7 +356,7 @@ describe('cy.origin - external hosts', { browser: '!webkit' }, () => {
cy.visit('https://www.foobar.com:3502/fixtures/primary-origin.html')
cy.origin('https://www.idp.com:3502', () => undefined)
cy.then(() => {
const expectedSrc = `https://www.idp.com:3502/__cypress/spec-bridge-iframes`
const expectedSrc = `https://www.idp.com:3502/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://www.idp.com:3502') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
@@ -372,7 +372,7 @@ describe('cy.origin - external hosts', { browser: '!webkit' }, () => {
cy.visit('https://www.google.com')
cy.origin('accounts.google.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://accounts.google.com/__cypress/spec-bridge-iframes`
const expectedSrc = `https://accounts.google.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://accounts.google.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)

View File

@@ -0,0 +1,191 @@
import { runImportedPrivilegedCommands } from '../../support/utils'
const isWebkit = Cypress.isBrowser({ family: 'webkit' })
function runSpecFunctionCommands () {
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
if (!isWebkit) {
cy.origin('http://foobar.com:3500', () => {})
}
}
Cypress.Commands.add('runSpecFileCustomPrivilegedCommands', runSpecFunctionCommands)
describe('privileged commands', () => {
describe('in spec file or support file', () => {
let ranInBeforeEach = false
beforeEach(() => {
if (ranInBeforeEach) return
ranInBeforeEach = true
// ensures these run properly in hooks, but only run it once per spec run
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
if (!isWebkit) {
cy.origin('http://foobar.com:3500', () => {})
}
})
it('passes in test body', () => {
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
if (!isWebkit) {
cy.origin('http://foobar.com:3500', () => {})
}
})
it('passes two or more exact commands in a row', () => {
cy.task('return:arg', 'arg')
cy.task('return:arg', 'arg')
})
it('passes in test body .then() callback', () => {
cy.then(() => {
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
if (!isWebkit) {
cy.origin('http://foobar.com:3500', () => {})
}
})
})
it('passes in spec function', () => {
runSpecFunctionCommands()
})
it('passes in imported function', () => {
runImportedPrivilegedCommands()
})
it('passes in support file global function', () => {
window.runGlobalPrivilegedCommands()
})
it('passes in spec file custom command', () => {
cy.runSpecFileCustomPrivilegedCommands()
})
it('passes in support file custom command', () => {
cy.runSupportFileCustomPrivilegedCommands()
})
// cy.origin() doesn't currently have webkit support
it('passes in .origin() callback', { browser: '!webkit' }, () => {
cy.origin('http://foobar.com:3500', () => {
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
// there's a bug using cy.selectFile() with a path inside of
// cy.origin(): https://github.com/cypress-io/cypress/issues/25261
// cy.visit('/fixtures/files-form.html')
// cy.get('#basic').selectFile('cypress/fixtures/valid.json')
})
})
})
describe('in AUT', () => {
const strategies = ['inline', 'then', 'eval', 'function']
const commands = ['exec', 'readFile', 'writeFile', 'selectFile', 'task']
// cy.origin() doesn't currently have webkit support
if (!Cypress.isBrowser({ family: 'webkit' })) {
commands.push('origin')
}
const errorForCommand = (commandName) => {
return `\`cy.${commandName}()\` must only be invoked from the spec file or support file.`
}
strategies.forEach((strategy) => {
commands.forEach((command) => {
describe(`strategy: ${strategy}`, () => {
describe(`command: ${command}`, () => {
it('fails in html script', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(errorForCommand(command))
done()
})
cy.visit(`/aut-commands?strategy=${strategy}&command=${command}`)
})
it('fails in separate script', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(errorForCommand(command))
done()
})
cy.visit(`/fixtures/aut-commands.html?strategy=${strategy}&command=${command}`)
})
it('does not run command in separate script appended to spec frame', () => {
let ranCommand = false
cy.on('log:added', (attrs) => {
if (attrs.name === command) {
ranCommand = true
}
})
// this attempts to run the command by appending a <script> to the
// spec frame, but the Content-Security-Policy we set will prevent
// that script from running
cy.visit(`/aut-commands?appendToSpecFrame=true&strategy=${strategy}&command=${command}`)
// wait 500ms then ensure the command did not run
cy.wait(500).then(() => {
expect(ranCommand, `expected cy.${command}() not to run, but it did`).to.be.false
})
})
// while not immediately obvious, this basically triggers using
// cy.origin() within itself. that doesn't work anyways and
// hits a different error, so it can't be used outside of the spec
// in this manner
if (command !== 'origin') {
// cy.origin() doesn't currently have webkit support
it('fails in cross-origin html script', { browser: '!webkit' }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(errorForCommand(command))
done()
})
cy.origin('http://foobar.com:3500', { args: { strategy, command } }, ({ strategy, command }) => {
cy.visit(`/aut-commands?strategy=${strategy}&command=${command}`)
})
})
// cy.origin() doesn't currently have webkit support
it('fails in cross-origin separate script', { browser: '!webkit' }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(errorForCommand(command))
done()
})
cy.origin('http://foobar.com:3500', { args: { strategy, command } }, ({ strategy, command }) => {
cy.visit(`/fixtures/aut-commands.html?strategy=${strategy}&command=${command}`)
})
})
}
})
})
})
})
})
})

View File

@@ -0,0 +1,2 @@
<input type="file" />
<script src="aut-commands.js"></script>

View File

@@ -0,0 +1,97 @@
(() => {
const urlParams = new URLSearchParams(window.__search || window.location.search)
const appendToSpecFrame = !!urlParams.get('appendToSpecFrame')
const strategy = urlParams.get('strategy')
const command = urlParams.get('command')
const cy = window.Cypress.cy
if (cy.state('current')) {
cy.state('current').attributes.args = [() => {}]
}
const TOP = 'top' // prevents frame-busting
// recursively tries sibling frames until finding the spec frame, which
// should be the first same-origin one we come across
const specFrame = window.__isSpecFrame ? window : (() => {
const tryFrame = (index) => {
try {
// will throw if cross-origin
window[TOP].frames[index].location.href
return window[TOP].frames[index]
} catch (err) {
return tryFrame(index + 1)
}
}
return tryFrame(1)
})()
const run = () => {
switch (command) {
case 'exec':
cy.exec('echo "Goodbye"')
break
case 'readFile':
cy.readFile('cypress/fixtures/example.json')
break
case 'writeFile':
cy.writeFile('cypress/_test-output/written.json', 'other contents')
break
case 'task':
cy.task('return:arg', 'other arg')
break
case 'selectFile':
cy.get('input').selectFile('cypress/fixtures/example.json')
break
case 'origin':
cy.origin('http://barbaz.com:3500', () => {})
break
default:
throw new Error(`Command not supported: ${command}`)
}
}
const runString = run.toString()
// instead of running this script in the AUT, this appends it to the
// spec frame to run it there
if (appendToSpecFrame) {
cy.wait(500) // gives the script time to run without the queue ending
const beforeScript = specFrame.document.createElement('script')
beforeScript.textContent = `
window.__search = '${window.location.search.replace('appendToSpecFrame=true&', '')}'
window.__isSpecFrame = true
`
specFrame.document.body.appendChild(beforeScript)
const scriptEl = specFrame.document.createElement('script')
scriptEl.src = '/fixtures/aut-commands.js'
specFrame.document.body.appendChild(scriptEl)
return
}
switch (strategy) {
case 'inline':
run()
break
case 'then':
cy.then(run)
break
case 'eval':
specFrame.eval(`(command) => { (${runString})() }`)(command)
break
case 'function': {
const fn = new specFrame.Function('command', `(${runString})()`)
fn.call(specFrame, command)
break
}
default:
throw new Error(`Strategy not supported: ${strategy}`)
}
})()

View File

@@ -1,4 +1,4 @@
const fs = require('fs')
const fs = require('fs-extra')
const auth = require('basic-auth')
const bodyParser = require('body-parser')
const express = require('express')
@@ -355,11 +355,24 @@ const createApp = (port) => {
const el = document.createElement('p')
el.id = 'p' + i
el.innerHTML = 'x'.repeat(100000)
document.body.appendChild(el)
}
</script>
</html>
</html>
`)
})
app.get('/aut-commands', async (req, res) => {
const script = (await fs.readFileAsync(path.join(__dirname, '..', 'fixtures', 'aut-commands.js'))).toString()
res.send(`
<html>
<body>
<input type="file" />
<script>${script}</script>
</body>
</html>
`)
})

View File

@@ -11,7 +11,9 @@ if (!isActuallyInteractive) {
Cypress.config('retries', 2)
}
beforeEach(() => {
let ranPrivilegedCommandsInBeforeEach = false
beforeEach(function () {
// always set that we're interactive so we
// get consistent passes and failures when running
// from CI and when running in GUI mode
@@ -30,6 +32,25 @@ beforeEach(() => {
try {
$(cy.state('window')).off()
} catch (error) {} // eslint-disable-line no-empty
// only want to run this as part of the privileged commands spec
if (cy.config('spec').baseName === 'privileged_commands.cy.ts') {
cy.visit('/fixtures/files-form.html')
// it only needs to run once per spec run
if (ranPrivilegedCommandsInBeforeEach) return
ranPrivilegedCommandsInBeforeEach = true
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
if (!Cypress.isBrowser({ family: 'webkit' })) {
cy.origin('http://foobar.com:3500', () => {})
}
}
})
// this is here to test that cy.origin() dependencies used directly in the

View File

@@ -172,6 +172,29 @@ export const makeRequestForCookieBehaviorTests = (
})
}
function runCommands () {
cy.exec('echo "hello"')
cy.readFile('cypress/fixtures/app.json')
cy.writeFile('cypress/_test-output/written.json', 'contents')
cy.task('return:arg', 'arg')
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
if (!Cypress.isBrowser({ family: 'webkit' })) {
cy.origin('http://foobar.com:3500', () => {})
}
}
export const runImportedPrivilegedCommands = runCommands
declare global {
interface Window {
runGlobalPrivilegedCommands: () => void
}
}
window.runGlobalPrivilegedCommands = runCommands
Cypress.Commands.add('runSupportFileCustomPrivilegedCommands', runCommands)
Cypress.Commands.addQuery('getAll', getAllFn)
Cypress.Commands.add('shouldWithTimeout', shouldWithTimeout)

1
packages/driver/foo.json Normal file
View File

@@ -0,0 +1 @@
{}

1
packages/driver/foo.txt Normal file
View File

@@ -0,0 +1 @@
contents

View File

@@ -167,6 +167,16 @@ export class PrimaryOriginCommunicator extends EventEmitter {
preprocessedData.args = data.args
}
// if the data has an error/err, it needs special handling for Firefox or
// else it will end up ignored because it's not structured-cloneable
if (data?.error) {
preprocessedData.error = preprocessForSerialization(data.error)
}
if (data?.err) {
preprocessedData.err = preprocessForSerialization(data.err)
}
// If there is no crossOriginDriverWindows, there is no need to send the message.
source.postMessage({
event,

View File

@@ -8,6 +8,10 @@ export const handleSocketEvents = (Cypress) => {
timeout: Cypress.config().defaultCommandTimeout,
})
if (response && response.error) {
return callback({ error: response.error })
}
callback({ response })
}

View File

@@ -181,9 +181,16 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
Cypress.specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncGlobals: true })
})
// the name of this function is used to verify if privileged commands are
// properly called. it shouldn't be removed and if the name is changed, it
// needs to also be changed in server/lib/browsers/privileged-channel.js
function invokeOriginFn (callback) {
return window.eval(`(${callback})`)(args)
}
try {
const callback = await getCallbackFn(fn, file)
const value = window.eval(`(${callback})`)(args)
const value = invokeOriginFn(callback)
// If we detect a non promise value with commands in queue, throw an error
if (value && cy.queue.length > 0 && !value.then) {

View File

@@ -6,6 +6,7 @@ import $dom from '../../../dom'
import $errUtils from '../../../cypress/error_utils'
import $actionability from '../../actionability'
import { addEventCoords, dispatch } from './trigger'
import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel'
/* dropzone.js relies on an experimental, nonstandard API, webkitGetAsEntry().
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
@@ -82,6 +83,15 @@ interface InternalSelectFileOptions extends Cypress.SelectFileOptions {
eventTarget: JQuery
}
interface FilePathObject {
fileName?: string
index: number
isFilePath: boolean
lastModified?: number
mimeType?: string
path: string
}
const ACTIONS = {
select: (element, dataTransfer, coords, state) => {
(element as HTMLInputElement).files = dataTransfer.files
@@ -153,32 +163,64 @@ export default (Commands, Cypress, cy, state, config) => {
}
}
// Uses backend read:file rather than cy.readFile because we don't want to retry
// loading a specific file until timeout, but rather retry the selectFile command as a whole
const handlePath = async (file, options) => {
return Cypress.backend('read:file', file.contents, { encoding: null })
.then(({ contents }) => {
return {
// We default to the filename on the path, but allow them to override
fileName: basename(file.contents),
...file,
contents: Cypress.Buffer.from(contents),
}
const readFiles = async (filePaths, options, userArgs) => {
if (!filePaths.length) return []
// This reads the file with privileged access in the same manner as
// cy.readFile(). We call directly into the backend rather than calling
// cy.readFile() directly because we don't want to retry loading a specific
// file until timeout, but rather retry the selectFile command as a whole
return runPrivilegedCommand({
commandName: 'selectFile',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
options: {
files: filePaths,
},
userArgs,
})
.then((results) => {
return results.map((result) => {
return {
// We default to the filename on the path, but allow them to override
fileName: basename(result.path),
...result,
contents: Cypress.Buffer.from(result.contents),
}
})
})
.catch((err) => {
if (err.isNonSpec) {
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
args: { cmd: 'selectFile' },
})
}
if (err.code === 'ENOENT') {
$errUtils.throwErrByPath('files.nonexistent', {
args: { cmd: 'selectFile', file: file.contents, filePath: err.filePath },
args: { cmd: 'selectFile', file: err.originalFilePath, filePath: err.filePath },
})
}
$errUtils.throwErrByPath('files.unexpected_error', {
onFail: options._log,
args: { cmd: 'selectFile', action: 'read', file, filePath: err.filePath, error: err.message },
args: { cmd: 'selectFile', action: 'read', file: err.originalFilePath, filePath: err.filePath, error: err.message },
})
})
}
const getFilePathObject = (file, index) => {
return {
encoding: null,
fileName: file.fileName,
index,
isFilePath: true,
lastModified: file.lastModified,
mimeType: file.mimeType,
path: file.contents,
}
}
/*
* Turns a user-provided file - a string shorthand, ArrayBuffer, or object
* into an object of form {
@@ -191,7 +233,7 @@ export default (Commands, Cypress, cy, state, config) => {
* we warn them and suggest how to fix it.
*/
const parseFile = (options) => {
return async (file: any, index: number, filesArray: any[]): Promise<Cypress.FileReferenceObject> => {
return (file: any, index: number, filesArray: any[]): Cypress.FileReferenceObject | FilePathObject => {
if (typeof file === 'string' || ArrayBuffer.isView(file)) {
file = { contents: file }
}
@@ -212,10 +254,13 @@ export default (Commands, Cypress, cy, state, config) => {
}
if (typeof file.contents === 'string') {
file = handleAlias(file, options) ?? await handlePath(file, options)
// if not an alias, an object representing that the file is a path that
// needs to be read from disk. contents are an empty string to they
// it skips the next check
file = handleAlias(file, options) ?? getFilePathObject(file, index)
}
if (!_.isString(file.contents) && !ArrayBuffer.isView(file.contents)) {
if (!file.isFilePath && !_.isString(file.contents) && !ArrayBuffer.isView(file.contents)) {
file.contents = JSON.stringify(file.contents)
}
@@ -223,8 +268,24 @@ export default (Commands, Cypress, cy, state, config) => {
}
}
async function collectFiles (files, options, userArgs) {
const filesCollection = ([] as (Cypress.FileReference | FilePathObject)[]).concat(files).map(parseFile(options))
// if there are any file paths, read them from the server in one go
const filePaths = filesCollection.filter((file) => (file as FilePathObject).isFilePath)
const filePathResults = await readFiles(filePaths, options, userArgs)
// stitch them back into the collection
filePathResults.forEach((filePathResult) => {
filesCollection[filePathResult.index] = _.pick(filePathResult, 'contents', 'fileName', 'mimeType', 'lastModified')
})
return filesCollection as Cypress.FileReferenceObject[]
}
Commands.addAll({ prevSubject: 'element' }, {
async selectFile (subject: JQuery<any>, files: Cypress.FileReference | Cypress.FileReference[], options: Partial<InternalSelectFileOptions>): Promise<JQuery> {
const userArgs = trimUserArgs([files, _.isObject(options) ? { ...options } : undefined])
options = _.defaults({}, options, {
action: 'select',
log: true,
@@ -287,8 +348,7 @@ export default (Commands, Cypress, cy, state, config) => {
}
// Make sure files is an array even if the user only passed in one
const filesArray = await Promise.all(([] as Cypress.FileReference[]).concat(files).map(parseFile(options)))
const filesArray = await collectFiles(files, options, userArgs)
const subjectChain = cy.subjectChain()
// We verify actionability on the subject, rather than the eventTarget,

View File

@@ -3,18 +3,24 @@ import Promise from 'bluebird'
import $errUtils from '../../cypress/error_utils'
import type { Log } from '../../cypress/log'
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
interface InternalExecOptions extends Partial<Cypress.ExecOptions> {
_log?: Log
cmd?: string
timeout: number
}
export default (Commands, Cypress, cy) => {
Commands.addAll({
exec (cmd: string, userOptions: Partial<Cypress.ExecOptions> = {}) {
exec (cmd: string, userOptions: Partial<Cypress.ExecOptions>) {
const userArgs = trimUserArgs([cmd, userOptions])
userOptions = userOptions || {}
const options: InternalExecOptions = _.defaults({}, userOptions, {
log: true,
timeout: Cypress.config('execTimeout'),
timeout: Cypress.config('execTimeout') as number,
failOnNonZeroExit: true,
env: {},
})
@@ -46,7 +52,13 @@ export default (Commands, Cypress, cy) => {
// because we're handling timeouts ourselves
cy.clearTimeout()
return Cypress.backend('exec', _.pick(options, 'cmd', 'timeout', 'env'))
return runPrivilegedCommand({
commandName: 'exec',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
options: _.pick(options, 'cmd', 'timeout', 'env'),
userArgs,
})
.timeout(options.timeout)
.then((result) => {
if (options._log) {
@@ -75,20 +87,26 @@ export default (Commands, Cypress, cy) => {
})
})
.catch(Promise.TimeoutError, { timedOut: true }, () => {
return $errUtils.throwErrByPath('exec.timed_out', {
$errUtils.throwErrByPath('exec.timed_out', {
onFail: options._log,
args: { cmd, timeout: options.timeout },
})
})
.catch((error) => {
.catch((err) => {
// re-throw if timedOut error from above
if (error.name === 'CypressError') {
throw error
if (err.name === 'CypressError') {
throw err
}
return $errUtils.throwErrByPath('exec.failed', {
if (err.isNonSpec) {
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
args: { cmd: 'exec' },
})
}
$errUtils.throwErrByPath('exec.failed', {
onFail: options._log,
args: { cmd, error },
args: { cmd, error: err },
})
})
},

View File

@@ -3,30 +3,37 @@ import { basename } from 'path'
import $errUtils from '../../cypress/error_utils'
import type { Log } from '../../cypress/log'
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
interface InternalWriteFileOptions extends Partial<Cypress.WriteFileOptions & Cypress.Timeoutable> {
_log?: Log
timeout: number
}
interface ReadFileOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
encoding?: Cypress.Encodings
}
interface InternalWriteFileOptions extends Partial<Cypress.WriteFileOptions & Cypress.Timeoutable> {
_log?: Log
}
type WriteFileOptions = Partial<Cypress.WriteFileOptions & Cypress.Timeoutable>
export default (Commands, Cypress, cy, state) => {
Commands.addQuery('readFile', function readFile (file, encoding, options: ReadFileOptions = {}) {
Commands.addQuery('readFile', function readFile (file: string, encoding: Cypress.Encodings | ReadFileOptions | undefined, userOptions?: ReadFileOptions) {
const userArgs = trimUserArgs([file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined])
if (_.isObject(encoding)) {
options = encoding
encoding = options.encoding
userOptions = encoding
encoding = userOptions.encoding
}
userOptions = userOptions || {}
encoding = encoding === undefined ? 'utf8' : encoding
const timeout = options.timeout ?? Cypress.config('defaultCommandTimeout')
const timeout = userOptions.timeout ?? Cypress.config('defaultCommandTimeout') as number
this.set('timeout', timeout)
this.set('ensureExistenceFor', 'subject')
const log = options.log !== false && Cypress.log({ message: file, timeout })
const log = userOptions.log !== false && Cypress.log({ message: file, timeout })
if (!file || !_.isString(file)) {
$errUtils.throwErrByPath('files.invalid_argument', {
@@ -48,7 +55,16 @@ export default (Commands, Cypress, cy, state) => {
}
fileResult = null
filePromise = Cypress.backend('read:file', file, { encoding })
filePromise = runPrivilegedCommand({
commandName: 'readFile',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
options: {
file,
encoding,
},
userArgs,
})
.timeout(timeout)
.then((result) => {
// https://github.com/cypress-io/cypress/issues/1558
@@ -72,6 +88,12 @@ export default (Commands, Cypress, cy, state) => {
})
}
if (err.isNonSpec) {
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
args: { cmd: 'readFile' },
})
}
// Non-ENOENT errors are not retried
if (err.code !== 'ENOENT') {
$errUtils.throwErrByPath('files.unexpected_error', {
@@ -130,12 +152,16 @@ export default (Commands, Cypress, cy, state) => {
})
Commands.addAll({
writeFile (fileName, contents, encoding, userOptions: Partial<Cypress.WriteFileOptions & Cypress.Timeoutable> = {}) {
writeFile (fileName: string, contents: string, encoding: Cypress.Encodings | WriteFileOptions | undefined, userOptions: WriteFileOptions) {
const userArgs = trimUserArgs([fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined])
if (_.isObject(encoding)) {
userOptions = encoding
encoding = undefined
}
userOptions = userOptions || {}
const options: InternalWriteFileOptions = _.defaults({}, userOptions, {
// https://github.com/cypress-io/cypress/issues/1558
// If no encoding is specified, then Cypress has historically defaulted
@@ -182,7 +208,19 @@ export default (Commands, Cypress, cy, state) => {
// the timeout ourselves
cy.clearTimeout()
return Cypress.backend('write:file', fileName, contents, _.pick(options, 'encoding', 'flag')).timeout(options.timeout)
return runPrivilegedCommand({
commandName: 'writeFile',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
options: {
fileName,
contents,
encoding: options.encoding,
flag: options.flag,
},
userArgs,
})
.timeout(options.timeout)
.then(({ filePath, contents }) => {
consoleProps['File Path'] = filePath
consoleProps['Contents'] = contents
@@ -197,6 +235,12 @@ export default (Commands, Cypress, cy, state) => {
})
}
if (err.isNonSpec) {
return $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
args: { cmd: 'writeFile' },
})
}
return $errUtils.throwErrByPath('files.unexpected_error', {
onFail: options._log,
args: { cmd: 'writeFile', action: 'write', file: fileName, filePath: err.filePath, error: err.message },

View File

@@ -9,6 +9,7 @@ import { $Location } from '../../../cypress/location'
import { LogUtils } from '../../../cypress/log'
import logGroup from '../../logGroup'
import type { StateFunc } from '../../../cypress/state'
import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel'
const reHttp = /^https?:\/\//
@@ -23,15 +24,32 @@ const normalizeOrigin = (urlOrDomain) => {
return $Location.normalize(origin)
}
type OptionsOrFn<T> = { args: T } | (() => {})
type Fn<T> = (args?: T) => {}
function stringifyFn (fn?: any) {
return _.isFunction(fn) ? fn.toString() : undefined
}
function getUserArgs<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>) {
return trimUserArgs([
urlOrDomain,
fn && _.isObject(optionsOrFn) ? { ...optionsOrFn } : stringifyFn(optionsOrFn),
fn ? stringifyFn(fn) : undefined,
])
}
export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: StateFunc, config: Cypress.InternalConfig) => {
const communicator = Cypress.primaryOriginCommunicator
Commands.addAll({
origin<T> (urlOrDomain: string, optionsOrFn: { args: T } | (() => {}), fn?: (args?: T) => {}) {
origin<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>) {
if (Cypress.isBrowser('webkit')) {
return $errUtils.throwErrByPath('webkit.origin')
}
const userArgs = getUserArgs<T>(urlOrDomain, optionsOrFn, fn)
const userInvocationStack = state('current').get('userInvocationStack')
// store the invocation stack in the case that `cy.origin` errors
@@ -185,9 +203,21 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
const fn = _.isFunction(callbackFn) ? callbackFn.toString() : callbackFn
const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, config('projectRoot'))?.absoluteFile
// once the secondary origin page loads, send along the
// user-specified callback to run in that origin
try {
// origin is a privileged command, meaning it has to be invoked
// from the spec or support file
await runPrivilegedCommand({
commandName: 'origin',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
options: {
specBridgeOrigin,
},
userArgs,
})
// once the secondary origin page loads, send along the
// user-specified callback to run in that origin
communicator.toSpecBridge(origin, 'run:origin:fn', {
args: options?.args || undefined,
fn,
@@ -212,6 +242,12 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
logCounter: LogUtils.getCounter(),
})
} catch (err: any) {
if (err.isNonSpec) {
return _reject($errUtils.errByPath('miscellaneous.non_spec_invocation', {
cmd: 'origin',
}))
}
const wrappedErr = $errUtils.errByPath('origin.run_origin_fn_errored', {
error: err.message,
})

View File

@@ -5,17 +5,23 @@ import $utils from '../../cypress/utils'
import $errUtils from '../../cypress/error_utils'
import $stackUtils from '../../cypress/stack_utils'
import type { Log } from '../../cypress/log'
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
interface InternalTaskOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
timeout: number
}
export default (Commands, Cypress, cy) => {
Commands.addAll({
task (task, arg, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
task (task, arg, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable>) {
const userArgs = trimUserArgs([task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined])
userOptions = userOptions || {}
const options: InternalTaskOptions = _.defaults({}, userOptions, {
log: true,
timeout: Cypress.config('taskTimeout'),
timeout: Cypress.config('taskTimeout') as number,
})
let consoleOutput
@@ -52,10 +58,16 @@ export default (Commands, Cypress, cy) => {
// because we're handling timeouts ourselves
cy.clearTimeout()
return Cypress.backend('task', {
task,
arg,
timeout: options.timeout,
return runPrivilegedCommand({
commandName: 'task',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
userArgs,
options: {
task,
arg,
timeout: options.timeout,
},
})
.timeout(options.timeout)
.then((result) => {
@@ -71,7 +83,7 @@ export default (Commands, Cypress, cy) => {
args: { task, timeout: options.timeout },
})
})
.catch({ timedOut: true }, (error) => {
.catch({ timedOut: true }, (error: any) => {
$errUtils.throwErrByPath('task.server_timed_out', {
onFail: options._log,
args: { task, timeout: options.timeout, error: error.message },
@@ -83,6 +95,12 @@ export default (Commands, Cypress, cy) => {
throw err
}
if (err.isNonSpec) {
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
args: { cmd: 'task' },
})
}
err.stack = $stackUtils.normalizedStack(err)
if (err?.isKnownError) {

View File

@@ -45,6 +45,7 @@ import { setupAutEventHandlers } from './cypress/aut_event_handlers'
import type { CachedTestState } from '@packages/types'
import * as cors from '@packages/network/lib/cors'
import { setSpecContentSecurityPolicy } from './util/privileged_channel'
import { telemetry } from '@packages/telemetry/src/browser'
@@ -56,6 +57,8 @@ declare global {
Cypress: Cypress.Cypress
Runner: any
cy: Cypress.cy
// eval doesn't exist on the built-in Window type for some reason
eval (expression: string): any
}
}
@@ -344,7 +347,17 @@ class $Cypress {
this.events.proxyTo(this.cy)
$scriptUtils.runScripts(specWindow, scripts)
$scriptUtils.runScripts({
browser: this.config('browser'),
scripts,
specWindow,
testingType: this.testingType,
})
.then(() => {
if (this.testingType === 'e2e') {
return setSpecContentSecurityPolicy(specWindow)
}
})
.catch((error) => {
this.runner.onSpecError('error')({ error })
})

View File

@@ -20,13 +20,15 @@ export class $Chainer {
static add (key, fn) {
$Chainer.prototype[key] = function (...args) {
const verificationPromise = Cypress.emitMap('command:invocation', { name: key, args })
const userInvocationStack = $stackUtils.normalizedUserInvocationStack(
(new this.specWindow.Error('command invocation stack')).stack,
)
// call back the original function with our new args
// pass args an as array and not a destructured invocation
fn(this, userInvocationStack, args)
fn(this, userInvocationStack, args, verificationPromise)
// return the chainer so additional calls
// are slurped up by the chainer instead of cy

View File

@@ -683,7 +683,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
const cyFn = wrap(true)
const chainerFn = wrap(false)
const callback = (chainer, userInvocationStack, args, firstCall = false) => {
const callback = (chainer, userInvocationStack, args, verificationPromise, firstCall = false) => {
// dont enqueue / inject any new commands if
// onInjectCommand returns false
const onInjectCommand = cy.state('onInjectCommand')
@@ -699,6 +699,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
chainerId: chainer.chainerId,
userInvocationStack,
fn: firstCall ? cyFn : chainerFn,
verificationPromise,
}))
}
@@ -707,6 +708,15 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
cy[name] = function (...args) {
ensureRunnable(cy, name)
// for privileged commands, we send a message to the server that verifies
// them as coming from the spec. the fulfillment of this promise means
// the message was received. the implementation for those commands
// checks to make sure this promise is fulfilled before sending its
// websocket message for running the command to ensure prevent a race
// condition where running the command happens before the command is
// verified
const verificationPromise = Cypress.emitMap('command:invocation', { name, args })
// this is the first call on cypress
// so create a new chainer instance
const chainer = new $Chainer(cy.specWindow)
@@ -717,7 +727,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
callback(chainer, userInvocationStack, args, true)
callback(chainer, userInvocationStack, args, verificationPromise, true)
// if we are in the middle of a command
// and its return value is a promise
@@ -761,7 +771,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
this.queryFns[name] = fn
const callback = (chainer, userInvocationStack, args) => {
const callback = (chainer, userInvocationStack, args, verificationPromise) => {
// dont enqueue / inject any new commands if
// onInjectCommand returns false
const onInjectCommand = cy.state('onInjectCommand')
@@ -791,6 +801,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
cyFn.originalFn = fn
command.set('fn', cyFn)
command.set('verificationPromise', verificationPromise)
cy.enqueue(command)
}
@@ -800,6 +811,15 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
cy[name] = function (...args) {
ensureRunnable(cy, name)
// for privileged commands, we send a message to the server that verifies
// them as coming from the spec. the fulfillment of this promise means
// the message was received. the implementation for those commands
// checks to make sure this promise is fulfilled before sending its
// websocket message for running the command to ensure prevent a race
// condition where running the command happens before the command is
// verified
const verificationPromise = Cypress.emitMap('command:invocation', { name, args })
// this is the first call on cypress
// so create a new chainer instance
const chainer = new $Chainer(cy.specWindow)
@@ -810,7 +830,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
callback(chainer, userInvocationStack, args)
callback(chainer, userInvocationStack, args, verificationPromise)
// if we're the first call onto a cy
// command, then kick off the run

View File

@@ -753,6 +753,7 @@ export default {
},
miscellaneous: {
non_spec_invocation: `${cmd('{{cmd}}')} must only be invoked from the spec file or support file.`,
returned_value_and_commands_from_custom_command (obj) {
return {
message: stripIndent`\

View File

@@ -42,9 +42,38 @@ const runScriptsFromUrls = (specWindow, scripts) => {
.then((scripts) => evalScripts(specWindow, scripts))
}
const appendScripts = (specWindow, scripts) => {
return Bluebird.each(scripts, (script: any) => {
const firstScript = specWindow.document.querySelector('script')
const specScript = specWindow.document.createElement('script')
return new Promise<void>((resolve) => {
specScript.addEventListener('load', () => {
resolve()
})
specScript.src = script.relativeUrl
firstScript.after(specScript)
})
})
}
interface Script {
absolute: string
relative: string
relativeUrl: string
}
interface RunScriptsOptions {
browser: Cypress.Browser
scripts: Script[]
specWindow: Window
testingType: Cypress.TestingType
}
// Supports either scripts as objects or as async import functions
export default {
runScripts: (specWindow, scripts) => {
runScripts: ({ browser, scripts, specWindow, testingType }: RunScriptsOptions) => {
// if scripts contains at least one promise
if (scripts.length && typeof scripts[0] === 'function') {
// chain the loading promises
@@ -54,6 +83,15 @@ export default {
return Bluebird.each(scripts, (script: any) => script())
}
// in webkit, stack traces for e2e are made pretty much useless if these
// scripts are eval'd, so we append them as script tags instead
if (browser.family === 'webkit' && testingType === 'e2e') {
return appendScripts(specWindow, scripts)
}
// for other browsers, we get the contents of the scripts so that we can
// extract and utilize the source maps for better errors and code frames.
// we then eval the script contents to run them
return runScriptsFromUrls(specWindow, scripts)
},
}

View File

@@ -0,0 +1,40 @@
import _ from 'lodash'
import Bluebird from 'bluebird'
/**
* prevents further scripts outside of our own and the spec itself from being
* run in the spec frame
* @param specWindow: Window
*/
export function setSpecContentSecurityPolicy (specWindow) {
const metaEl = specWindow.document.createElement('meta')
metaEl.setAttribute('http-equiv', 'Content-Security-Policy')
metaEl.setAttribute('content', `script-src 'unsafe-eval'`)
specWindow.document.querySelector('head')!.appendChild(metaEl)
}
interface RunPrivilegedCommandOptions {
commandName: string
cy: Cypress.cy
Cypress: InternalCypress.Cypress
options: any
userArgs: any[]
}
export function runPrivilegedCommand ({ commandName, cy, Cypress, options, userArgs }: RunPrivilegedCommandOptions): Bluebird<any> {
return Bluebird.try(() => {
return cy.state('current').get('verificationPromise')[0]
})
.then(() => {
return Cypress.backend('run:privileged', {
commandName,
options,
userArgs,
})
})
}
export function trimUserArgs (args: any[]) {
return _.dropRightWhile(args, _.isUndefined)
}

View File

@@ -68,7 +68,9 @@ declare namespace Cypress {
}
declare namespace InternalCypress {
interface Cypress extends Cypress.Cypress, NodeEventEmitter {}
interface Cypress extends Cypress.Cypress, NodeEventEmitter {
backend: (eventName: string, ...args: any[]) => Promise<any>
}
interface LocalStorage extends Cypress.LocalStorage {
setStorages: (local, remote) => LocalStorage

View File

@@ -5,5 +5,7 @@ declare namespace Cypress {
originLoadUtils(origin: string): Chainable
getAll(...aliases: string[]): Chainable
shouldWithTimeout(cb: (subj: {}) => void, timeout?: number): Chainable
runSpecFileCustomPrivilegedCommands(): Chainable
runSupportFileCustomPrivilegedCommands(): Chainable
}
}

View File

@@ -82,6 +82,11 @@
"from": "default",
"field": "execTimeout"
},
{
"value": false,
"from": "default",
"field": "experimentalCspAllowList"
},
{
"value": false,
"from": "default",

View File

@@ -544,6 +544,10 @@
"experiments": {
"title": "Experiments",
"description": "If you'd like to try out new features that we're working on, you can enable beta features for your project by turning on the experimental features you'd like to try. {0}",
"experimentalCspAllowList": {
"name": "CSP Allow List",
"description": "Enables Cypress to selectively permit Content-Security-Policy and Content-Security-Policy-Report-Only header directives, including those that might otherwise block Cypress from running."
},
"experimentalFetchPolyfill": {
"name": "Fetch polyfill",
"description": "Automatically replaces `window.fetch` with a polyfill that Cypress can spy on and stub. Note: `experimentalFetchPolyfill` has been deprecated in Cypress 6.0.0 and will be removed in a future release. Consider using [`cy.intercept()`](https://on.cypress.io/intercept) to intercept `fetch` requests instead."

View File

@@ -52,7 +52,7 @@ enum BrowserStatus {
}
"""
When we don't have an immediate response for the cloudViewer request, we'll use this as a fallback to
When we don't have an immediate response for the cloudViewer request, we'll use this as a fallback to
render the avatar in the header bar / signal authenticated state immediately
"""
type CachedUser implements Node {
@@ -1702,9 +1702,6 @@ type Mutation {
"""Ping configured Base URL"""
pingBaseUrl: Query
"""Removes the cache entries for specified cloudSpecByPath query records"""
purgeCloudSpecByPathCache(projectSlug: String!, specPaths: [String!]!): Boolean
"""show the launchpad windows"""
reconfigureProject: Boolean!
@@ -2055,12 +2052,18 @@ type RelevantRun {
"""Information about the current commit for the local project"""
currentCommitInfo: CommitInfo
"""Latest relevant runs to fetch for the specs and runs page"""
latest: [RelevantRunInfo!]!
"""Run number of the selected run in use on the Debug page"""
selectedRunNumber: Int
}
"""runNumber and commitSha for a given run"""
type RelevantRunInfo {
"""The run id"""
runId: ID!
"""The runNumber that these spec counts belong to"""
runNumber: Int!
@@ -2073,7 +2076,9 @@ type RelevantRunInfo {
enum RelevantRunLocationEnum {
DEBUG
RUNS
SIDEBAR
SPECS
}
"""
@@ -2345,11 +2350,6 @@ type Subscription {
"""Issued when the watched specs for the project changes"""
specsChange: CurrentProject
"""
Initiates the polling mechanism with the Cypress Cloud to check if we should refetch specs, and mark specs as stale if we have updates
"""
startPollingForSpecs(branchName: String, projectId: String): String
}
enum SupportStatusEnum {

View File

@@ -22,12 +22,15 @@ export const nexusSlowGuardPlugin = plugin({
if (isPromiseLike(result) && threshold !== false) {
const resolvePath = pathToArray(info.path)
const start = process.hrtime.bigint()
const hanging = setTimeout(() => {
const operationId = `${info.operation.operation} ${info.operation.name?.value ?? `(anonymous)`}`
if (process.env.CYPRESS_INTERNAL_ENV !== 'production') {
const totalMS = (process.hrtime.bigint() - start) / BigInt(1000000)
// eslint-disable-next-line no-console
console.error(chalk.red(`\n\nNexusSlowGuard: Taking more than ${threshold}ms to execute ${JSON.stringify(resolvePath)} for ${operationId}\n\n`))
console.error(chalk.red(`\n\nNexusSlowGuard: Taking more than ${threshold}ms to execute ${JSON.stringify(resolvePath)} for ${operationId} (total time ${totalMS}ms)\n\n`))
}
}, threshold)

View File

@@ -8,13 +8,10 @@ import { GenerateSpecResponse } from './gql-GenerateSpecResponse'
import { Cohort, CohortInput } from './gql-Cohorts'
import { Query } from './gql-Query'
import { ScaffoldedFile } from './gql-ScaffoldedFile'
import debugLib from 'debug'
import { ReactComponentResponse } from './gql-ReactComponentResponse'
import { TestsBySpecInput } from '../inputTypes'
import { RunSpecResult } from '../unions'
const debug = debugLib('cypress:graphql:mutation')
export const mutation = mutationType({
definition (t) {
t.field('copyTextToClipboard', {
@@ -681,25 +678,6 @@ export const mutation = mutationType({
},
})
t.field('purgeCloudSpecByPathCache', {
type: 'Boolean',
args: {
projectSlug: nonNull(stringArg({})),
specPaths: nonNull(list(nonNull(stringArg({})))),
},
description: 'Removes the cache entries for specified cloudSpecByPath query records',
resolve: async (source, args, ctx) => {
const { projectSlug, specPaths } = args
debug('Purging %d `cloudSpecByPath` cache records for project %s: %o', specPaths.length, projectSlug, specPaths)
for (let specPath of specPaths) {
await ctx.cloud.invalidate('Query', 'cloudSpecByPath', { projectSlug, specPath })
}
return true
},
})
t.field('refetchRemote', {
type: Query,
description: 'Signal that we are explicitly refetching remote data and should not use the server cache',

View File

@@ -4,6 +4,10 @@ export const RelevantRunInfo = objectType({
name: 'RelevantRunInfo',
description: 'runNumber and commitSha for a given run',
definition (t) {
t.nonNull.id('runId', {
description: 'The run id',
})
t.nonNull.int('runNumber', {
description: 'The runNumber that these spec counts belong to',
})
@@ -28,6 +32,11 @@ export const RelevantRun = objectType({
description: 'All relevant runs to fetch for the debug page prior to the latest completed run',
})
t.nonNull.list.nonNull.field('latest', {
type: RelevantRunInfo,
description: 'Latest relevant runs to fetch for the specs and runs page',
})
t.nonNull.int('commitsAhead', {
description: 'How many commits ahead the current local commit is from the commit of the current run',
})

Some files were not shown because too many files have changed in this diff Show More