From 71c5b864ea84c73b561ffaa15eadb94cb7de6422 Mon Sep 17 00:00:00 2001
From: Preston Goforth
Date: Wed, 14 Jun 2023 15:54:52 -0400
Subject: [PATCH 1/4] feat: Selective CSP header stripping from HTTPResponse
(#26483)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: Selective CSP header directive stripping from HTTPResponse
- uses `stripCspDirectives` config option
* feat: Selective CSP header directive permission from HTTPResponse
- uses `experimentalCspAllowList` config option
* Address Review Comments:
- Add i18n for `experimentalCspAllowList`
- Remove PR link in changelog
- Fix docs link in changelog
- Remove extra typedef additions
- Update validation error message and snapshot
- Fix middleware negated conditional
* chore: refactor driver test into system tests to get better test
coverage on experimentalCspAllowList options
* Address Review Comments:
- Remove legacyOption for `experimentalCspAllowList`
- Update App desc for `experimentalCspAllowList` to include "Content-Security-Policy-Report-Only"
- Modify CHANGELOG wording
- Specify “never” overrideLevel
- Remove unused validator (+2 squashed commits)
- Add "Addresses" note in CHANGELOG to satisfy automation
- Set `canUpdateDuringTestTime` to `false` to prevent confusion
* chore: Add `frame-src` and `child-src` to conditional CSP directives
* chore: Rename `isSubsetOf` to `isArrayIncludingAny`
* chore: fix CLI linting types
* chore: fix server unit tests
* chore: fix system tests within firefox and webkit
* chore: add form-action test
* chore: update system test snapshots
* chore: skip tests in webkit due to form-action flakiness
* chore: Move 'sandbox' and 'navigate-to' into `unsupportedCSPDirectives`
- Add additional system tests
- Update snapshots and unit test
* chore: update system test snapshots
* chore: fix system tests
* chore: do not run csp tests within firefox or webkit due to flake issues in CI
* chore: attempt to increase intercept delay to avoid race condition
* chore: update new snapshots with video defaults work
* chore: update changelog
---------
Co-authored-by: Bill Glesias
Co-authored-by: Matt Schile
---
cli/CHANGELOG.md | 1 +
cli/types/cypress.d.ts | 15 +
packages/app/cypress.config.ts | 1 +
...ql-CloudViewerAndProject_RequiredData.json | 5 +
.../gql-HeaderBar_HeaderBarQuery.json | 5 +
.../debug-Failing/gql-SpecsPageContainer.json | 5 +
...ql-CloudViewerAndProject_RequiredData.json | 5 +
.../gql-HeaderBar_HeaderBarQuery.json | 5 +
.../debug-Passing/gql-SpecsPageContainer.json | 5 +
.../config/__snapshots__/index.spec.ts.js | 3 +
.../__snapshots__/validation.spec.ts.js | 25 +
packages/config/src/options.ts | 6 +
packages/config/src/validation.ts | 44 +-
packages/config/test/project/utils.spec.ts | 30 ++
packages/config/test/validation.spec.ts | 81 ++++
.../driver/cypress/e2e/e2e/csp_headers.cy.js | 30 ++
.../e2e/e2e/origin/commands/waiting.cy.ts | 2 +-
.../cypress/fixtures/config.json | 5 +
.../frontend-shared/src/locales/en-US.json | 4 +
.../proxy/lib/http/response-middleware.ts | 71 ++-
packages/proxy/lib/http/util/csp-header.ts | 127 +++++
packages/proxy/lib/http/util/inject.ts | 16 +-
packages/proxy/lib/http/util/rewriter.ts | 5 +
packages/proxy/lib/types.ts | 1 +
.../test/integration/net-stubbing.spec.ts | 161 ++++++-
.../unit/http/response-middleware.spec.ts | 452 +++++++++++++++++-
.../test/unit/http/util/csp-header.spec.ts | 143 ++++++
packages/server/index.d.ts | 1 +
.../test/integration/http_requests_spec.js | 325 ++++++++++++-
packages/server/test/unit/config_spec.js | 58 +++
.../experimental_csp_allow_list_spec.ts.js | 354 ++++++++++++++
.../projects/e2e/csp_script_test.html | 26 +
.../form_action_with_allow_list_custom.cy.ts | 19 +
.../with_allow_list_custom.cy.ts | 103 ++++
.../with_allow_list_custom_or_true.cy.ts | 107 +++++
.../with_allow_list_true.cy.ts | 73 +++
.../projects/e2e/static/csp_styles.css | 3 +
.../test/experimental_csp_allow_list_spec.ts | 114 +++++
38 files changed, 2421 insertions(+), 15 deletions(-)
create mode 100644 packages/driver/cypress/e2e/e2e/csp_headers.cy.js
create mode 100644 packages/proxy/lib/http/util/csp-header.ts
create mode 100644 packages/proxy/test/unit/http/util/csp-header.spec.ts
create mode 100644 system-tests/__snapshots__/experimental_csp_allow_list_spec.ts.js
create mode 100644 system-tests/projects/e2e/csp_script_test.html
create mode 100644 system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/form_action_with_allow_list_custom.cy.ts
create mode 100644 system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom.cy.ts
create mode 100644 system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts
create mode 100644 system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_true.cy.ts
create mode 100644 system-tests/projects/e2e/static/csp_styles.css
create mode 100644 system-tests/test/experimental_csp_allow_list_spec.ts
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 1597ec1e67..31b72ee2ac 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -6,6 +6,7 @@ _Released 06/20/2023 (PENDING)_
**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).
**Bugfixes:**
diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts
index 804895976b..c59a94f1f7 100644
--- a/cli/types/cypress.d.ts
+++ b/cli/types/cypress.d.ts
@@ -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'
/**
@@ -3051,6 +3053,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/configuration#experimentalCspAllowList
+ * @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
diff --git a/packages/app/cypress.config.ts b/packages/app/cypress.config.ts
index b505c98ee9..adb652b825 100644
--- a/packages/app/cypress.config.ts
+++ b/packages/app/cypress.config.ts
@@ -12,6 +12,7 @@ export default defineConfig({
reporterOptions: {
configFile: '../../mocha-reporter-config.json',
},
+ experimentalCspAllowList: false,
experimentalInteractiveRunEvents: true,
component: {
experimentalSingleTabRunMode: true,
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json b/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json
index cbc94dc8e9..df9bf17111 100644
--- a/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json
+++ b/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json
@@ -83,6 +83,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json b/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json
index 2dfc59d031..c9c094137b 100644
--- a/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json
+++ b/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json
@@ -59,6 +59,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json b/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json
index d0907f4ffb..9ffbedf671 100644
--- a/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json
+++ b/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json
@@ -444,6 +444,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json b/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json
index 582bab6444..1f982a1f7a 100644
--- a/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json
+++ b/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json
@@ -83,6 +83,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json b/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json
index 2dfc59d031..c9c094137b 100644
--- a/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json
+++ b/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json
@@ -59,6 +59,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json b/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json
index 2398607a62..81ff604e86 100644
--- a/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json
+++ b/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json
@@ -1445,6 +1445,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/config/__snapshots__/index.spec.ts.js b/packages/config/__snapshots__/index.spec.ts.js
index 51e0c8b4f8..ee999fa833 100644
--- a/packages/config/__snapshots__/index.spec.ts.js
+++ b/packages/config/__snapshots__/index.spec.ts.js
@@ -34,6 +34,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
},
'env': {},
'execTimeout': 60000,
+ 'experimentalCspAllowList': false,
'experimentalFetchPolyfill': false,
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
@@ -121,6 +122,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
},
'env': {},
'execTimeout': 60000,
+ 'experimentalCspAllowList': false,
'experimentalFetchPolyfill': false,
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
@@ -204,6 +206,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key
'e2e',
'env',
'execTimeout',
+ 'experimentalCspAllowList',
'experimentalFetchPolyfill',
'experimentalInteractiveRunEvents',
'experimentalRunAllSpecs',
diff --git a/packages/config/__snapshots__/validation.spec.ts.js b/packages/config/__snapshots__/validation.spec.ts.js
index ebd24b7fcf..ace0c97230 100644
--- a/packages/config/__snapshots__/validation.spec.ts.js
+++ b/packages/config/__snapshots__/validation.spec.ts.js
@@ -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,
diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts
index 66eab7fa18..3128d671f7 100644
--- a/packages/config/src/options.ts
+++ b/packages/config/src/options.ts
@@ -198,6 +198,12 @@ const driverConfigOptions: Array = [
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,
diff --git a/packages/config/src/validation.ts b/packages/config/src/validation.ts
index 49f2f7eeb4..1cbcb34c71 100644
--- a/packages/config/src/validation.ts
+++ b/packages/config/src/validation.ts
@@ -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
}
diff --git a/packages/config/test/project/utils.spec.ts b/packages/config/test/project/utils.spec.ts
index 87c713d06e..d76d8f239a 100644
--- a/packages/config/test/project/utils.spec.ts
+++ b/packages/config/test/project/utils.spec.ts
@@ -859,6 +859,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) => {
@@ -1053,6 +1081,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' },
@@ -1150,6 +1179,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' },
diff --git a/packages/config/test/validation.spec.ts b/packages/config/test/validation.spec.ts
index 36bc180d55..05af3d8d8d 100644
--- a/packages/config/test/validation.spec.ts
+++ b/packages/config/test/validation.spec.ts
@@ -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
diff --git a/packages/driver/cypress/e2e/e2e/csp_headers.cy.js b/packages/driver/cypress/e2e/e2e/csp_headers.cy.js
new file mode 100644
index 0000000000..d6ca6a1974
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/csp_headers.cy.js
@@ -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')
+ })
+ })
+})
diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/waiting.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/waiting.cy.ts
index 20d62ed577..66571c82f9 100644
--- a/packages/driver/cypress/e2e/e2e/origin/commands/waiting.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/commands/waiting.cy.ts
@@ -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')
diff --git a/packages/frontend-shared/cypress/fixtures/config.json b/packages/frontend-shared/cypress/fixtures/config.json
index 22b4248613..cdaec6de85 100644
--- a/packages/frontend-shared/cypress/fixtures/config.json
+++ b/packages/frontend-shared/cypress/fixtures/config.json
@@ -82,6 +82,11 @@
"from": "default",
"field": "execTimeout"
},
+ {
+ "value": false,
+ "from": "default",
+ "field": "experimentalCspAllowList"
+ },
{
"value": false,
"from": "default",
diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json
index 8e78ad0c5f..d964c64cd5 100644
--- a/packages/frontend-shared/src/locales/en-US.json
+++ b/packages/frontend-shared/src/locales/en-US.json
@@ -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."
diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts
index 2eaf17e71d..d4f6bb36d7 100644
--- a/packages/proxy/lib/http/response-middleware.ts
+++ b/packages/proxy/lib/http/response-middleware.ts
@@ -1,4 +1,5 @@
import charset from 'charset'
+import crypto from 'crypto'
import iconv from 'iconv-lite'
import _ from 'lodash'
import { PassThrough, Readable } from 'stream'
@@ -19,6 +20,8 @@ import type { CypressIncomingRequest, CypressOutgoingResponse } from '@packages/
import type { HttpMiddleware, HttpMiddlewareThis } from '.'
import type { IncomingMessage, IncomingHttpHeaders } from 'http'
+import { cspHeaderNames, generateCspDirectives, nonceDirectives, parseCspHeaders, problematicCspDirectives, unsupportedCSPDirectives } from './util/csp-header'
+
export interface ResponseMiddlewareProps {
/**
* Before using `res.incomingResStream`, `prepareResStream` can be used
@@ -345,6 +348,41 @@ const SetInjectionLevel: ResponseMiddleware = function () {
// We set the header here only for proxied requests that have scripts injected that set the domain.
// Other proxied requests are ignored.
this.res.setHeader('Origin-Agent-Cluster', '?0')
+
+ // In order to allow the injected script to run on sites with a CSP header
+ // we must add a generated `nonce` into the response headers
+ const nonce = crypto.randomBytes(16).toString('base64')
+
+ // Iterate through each CSP header
+ cspHeaderNames.forEach((headerName) => {
+ const policyArray = parseCspHeaders(this.res.getHeaders(), headerName)
+ const usedNonceDirectives = nonceDirectives
+ // If there are no used CSP directives that restrict script src execution, our script will run
+ // without the nonce, so we will not add it to the response
+ .filter((directive) => policyArray.some((policyMap) => policyMap.has(directive)))
+
+ if (usedNonceDirectives.length) {
+ // If there is a CSP directive that that restrict script src execution, we must add the
+ // nonce policy to each supported directive of each CSP header. This is due to the effect
+ // of [multiple policies](https://w3c.github.io/webappsec-csp/#multiple-policies) in CSP.
+ this.res.injectionNonce = nonce
+ const modifiedCspHeader = policyArray.map((policies) => {
+ usedNonceDirectives.forEach((availableNonceDirective) => {
+ if (policies.has(availableNonceDirective)) {
+ const cspScriptSrc = policies.get(availableNonceDirective) || []
+
+ // We are mutating the policy map, and we will set it back to the response headers later
+ policies.set(availableNonceDirective, [...cspScriptSrc, `'nonce-${nonce}'`])
+ }
+ })
+
+ return policies
+ }).map(generateCspDirectives)
+
+ // To replicate original response CSP headers, we must apply all header values as an array
+ this.res.setHeader(headerName, modifiedCspHeader)
+ }
+ })
}
this.res.wantsSecurityRemoved = (this.config.modifyObstructiveCode || this.config.experimentalModifyObstructiveThirdPartyCode) &&
@@ -403,13 +441,39 @@ const OmitProblematicHeaders: ResponseMiddleware = function () {
'x-frame-options',
'content-length',
'transfer-encoding',
- 'content-security-policy',
- 'content-security-policy-report-only',
'connection',
])
this.res.set(headers)
+ if (this.config.experimentalCspAllowList) {
+ const allowedDirectives = this.config.experimentalCspAllowList === true ? [] : this.config.experimentalCspAllowList as Cypress.experimentalCspAllowedDirectives[]
+
+ // If the user has specified CSP directives to allow, we must not remove them from the CSP headers
+ const stripDirectives = [...unsupportedCSPDirectives, ...problematicCspDirectives.filter((directive) => !allowedDirectives.includes(directive))]
+
+ // Iterate through each CSP header
+ cspHeaderNames.forEach((headerName) => {
+ const modifiedCspHeaders = parseCspHeaders(this.incomingRes.headers, headerName, stripDirectives)
+ .map(generateCspDirectives)
+ .filter(Boolean)
+
+ if (modifiedCspHeaders.length === 0) {
+ // If there are no CSP policies after stripping directives, we will remove it from the response
+ // Altering the CSP headers using the native response header methods is case-insensitive
+ this.res.removeHeader(headerName)
+ } else {
+ // To replicate original response CSP headers, we must apply all header values as an array
+ this.res.setHeader(headerName, modifiedCspHeaders)
+ }
+ })
+ } else {
+ cspHeaderNames.forEach((headerName) => {
+ // Altering the CSP headers using the native response header methods is case-insensitive
+ this.res.removeHeader(headerName)
+ })
+ }
+
this.next()
}
@@ -634,6 +698,7 @@ const MaybeInjectHtml: ResponseMiddleware = function () {
const decodedBody = iconv.decode(body, nodeCharset)
const injectedBody = await rewriter.html(decodedBody, {
+ cspNonce: this.res.injectionNonce,
domainName: cors.getDomainNameFromUrl(this.req.proxiedUrl),
wantsInjection: this.res.wantsInjection,
wantsSecurityRemoved: this.res.wantsSecurityRemoved,
@@ -735,8 +800,8 @@ export default {
AttachPlainTextStreamFn,
InterceptResponse,
PatchExpressSetHeader,
+ OmitProblematicHeaders, // Since we might modify CSP headers, this middleware needs to come BEFORE SetInjectionLevel
SetInjectionLevel,
- OmitProblematicHeaders,
MaybePreventCaching,
MaybeStripDocumentDomainFeaturePolicy,
MaybeCopyCookiesFromIncomingRes,
diff --git a/packages/proxy/lib/http/util/csp-header.ts b/packages/proxy/lib/http/util/csp-header.ts
new file mode 100644
index 0000000000..94bc14dc1a
--- /dev/null
+++ b/packages/proxy/lib/http/util/csp-header.ts
@@ -0,0 +1,127 @@
+import type { OutgoingHttpHeaders } from 'http'
+
+const cspRegExp = /[; ]*([^\n\r; ]+) ?([^\n\r;]+)*/g
+
+export const cspHeaderNames = ['content-security-policy', 'content-security-policy-report-only'] as const
+
+export const nonceDirectives = ['script-src-elem', 'script-src', 'default-src']
+
+export const problematicCspDirectives = [
+ ...nonceDirectives,
+ 'child-src', 'frame-src', 'form-action',
+] as Cypress.experimentalCspAllowedDirectives[]
+
+export const unsupportedCSPDirectives = [
+ /**
+ * In order for Cypress to run content in an iframe, we must remove the `frame-ancestors` directive
+ * from the CSP header. This is because this directive behaves like the `X-Frame-Options='deny'` header
+ * and prevents the iframe content from being loaded if it detects that it is not being loaded in the
+ * top-level frame.
+ */
+ 'frame-ancestors',
+ /**
+ * The `navigate-to` directive is not yet fully supported, so we are erring on the side of caution
+ */
+ 'navigate-to',
+ /**
+ * The `sandbox` directive seems to affect all iframes on the page, even if the page is a direct child of Cypress
+ */
+ 'sandbox',
+ /**
+ * Since Cypress might modify the DOM of the application under test, `trusted-types` would prevent the
+ * DOM injection from occurring.
+ */
+ 'trusted-types',
+ 'require-trusted-types-for',
+]
+
+const caseInsensitiveGetAllHeaders = (headers: OutgoingHttpHeaders, lowercaseProperty: string): string[] => {
+ return Object.entries(headers).reduce((acc: string[], [key, value]) => {
+ if (key.toLowerCase() === lowercaseProperty) {
+ // It's possible to set more than 1 CSP header, and in those instances CSP headers
+ // are NOT merged by the browser. Instead, the most **restrictive** CSP header
+ // that applies to the given resource will be used.
+ // https://www.w3.org/TR/CSP2/#content-security-policy-header-field
+ //
+ // Therefore, we need to return each header as it's own value so we can apply
+ // injection nonce values to each one, because we don't know which will be
+ // the most restrictive.
+ acc.push.apply(
+ acc,
+ `${value}`.split(',')
+ .filter(Boolean)
+ .map((policyString) => `${policyString}`.trim()),
+ )
+ }
+
+ return acc
+ }, [])
+}
+
+function getCspHeaders (headers: OutgoingHttpHeaders, headerName: string = 'content-security-policy'): string[] {
+ return caseInsensitiveGetAllHeaders(headers, headerName.toLowerCase())
+}
+
+/**
+ * Parses the provided headers object and returns an array of policy Map objects.
+ * This will parse all CSP headers that match the provided `headerName` parameter,
+ * even if they are not lower case.
+ * @param headers - The headers object to parse
+ * @param headerName - The name of the header to parse. Defaults to `content-security-policy`
+ * @param excludeDirectives - An array of directives to exclude from the returned policy maps
+ * @returns An array of policy Map objects
+ *
+ * @example
+ * const policyMaps = parseCspHeaders({
+ * 'Content-Security-Policy': 'default-src self; script-src self https://www.google-analytics.com',
+ * 'content-security-policy': 'default-src self; script-src https://www.mydomain.com',
+ * })
+ * // policyMaps = [
+ * // Map {
+ * // 'default-src' => [ 'self' ],
+ * // 'script-src' => [ 'self', 'https://www.google-analytics.com' ]
+ * // },
+ * // Map {
+ * // 'default-src' => [ 'self' ],
+ * // 'script-src' => [ 'https://www.mydomain.com' ]
+ * // }
+ * // ]
+ */
+export function parseCspHeaders (headers: OutgoingHttpHeaders, headerName: string = 'content-security-policy', excludeDirectives: string[] = []): Map[] {
+ const cspHeaders = getCspHeaders(headers, headerName)
+
+ // We must make an policy map for each CSP header individually
+ return cspHeaders.reduce((acc: Map[], cspHeader) => {
+ const policies = new Map()
+ let policy = cspRegExp.exec(cspHeader)
+
+ while (policy) {
+ const [/* regExpMatch */, directive, values = ''] = policy
+
+ if (!excludeDirectives.includes(directive)) {
+ const currentDirective = policies.get(directive) || []
+
+ policies.set(directive, [...currentDirective, ...values.split(' ').filter(Boolean)])
+ }
+
+ policy = cspRegExp.exec(cspHeader)
+ }
+
+ return [...acc, policies]
+ }, [])
+}
+
+/**
+ * Generates a CSP header string from the provided policy map.
+ * @param policies - The policy map to generate the CSP header string from
+ * @returns A CSP header policy string
+ * @example
+ * const policyString = generateCspHeader(new Map([
+ * ['default-src', ['self']],
+ * ['script-src', ['self', 'https://www.google-analytics.com']],
+ * ]))
+ * // policyString = 'default-src self; script-src self https://www.google-analytics.com'
+ */
+export function generateCspDirectives (policies: Map): string {
+ return Array.from(policies.entries()).map(([directive, values]) => `${directive} ${values.join(' ')}`).join('; ')
+}
diff --git a/packages/proxy/lib/http/util/inject.ts b/packages/proxy/lib/http/util/inject.ts
index 936cf34eb3..7046657b4e 100644
--- a/packages/proxy/lib/http/util/inject.ts
+++ b/packages/proxy/lib/http/util/inject.ts
@@ -3,6 +3,7 @@ import { getRunnerInjectionContents, getRunnerCrossOriginInjectionContents } fro
import type { SerializableAutomationCookie } from '@packages/server/lib/util/cookies'
interface InjectionOpts {
+ cspNonce?: string
shouldInjectDocumentDomain: boolean
}
interface FullCrossOriginOpts {
@@ -11,6 +12,12 @@ interface FullCrossOriginOpts {
simulatedCookies: SerializableAutomationCookie[]
}
+function injectCspNonce (options: InjectionOpts) {
+ const { cspNonce } = options
+
+ return cspNonce ? ` nonce="${cspNonce}"` : ''
+}
+
export function partial (domain, options: InjectionOpts) {
let documentDomainInjection = `document.domain = '${domain}';`
@@ -21,7 +28,7 @@ export function partial (domain, options: InjectionOpts) {
// With useDefaultDocumentDomain=true we continue to inject an empty script tag in order to be consistent with our other forms of injection.
// This is also diagnostic in nature is it will allow us to debug easily to make sure injection is still occurring.
return oneLine`
-
`
@@ -36,7 +43,7 @@ export function full (domain, options: InjectionOpts) {
}
return oneLine`
-
`
}
diff --git a/packages/proxy/lib/http/util/rewriter.ts b/packages/proxy/lib/http/util/rewriter.ts
index 26067bce9e..de4d286a77 100644
--- a/packages/proxy/lib/http/util/rewriter.ts
+++ b/packages/proxy/lib/http/util/rewriter.ts
@@ -14,6 +14,7 @@ export type SecurityOpts = {
}
export type InjectionOpts = {
+ cspNonce?: string
domainName: string
wantsInjection: CypressWantsInjection
wantsSecurityRemoved: any
@@ -32,6 +33,7 @@ function getRewriter (useAstSourceRewriting: boolean) {
function getHtmlToInject (opts: InjectionOpts & SecurityOpts) {
const {
+ cspNonce,
domainName,
wantsInjection,
modifyObstructiveThirdPartyCode,
@@ -44,9 +46,11 @@ function getHtmlToInject (opts: InjectionOpts & SecurityOpts) {
case 'full':
return inject.full(domainName, {
shouldInjectDocumentDomain,
+ cspNonce,
})
case 'fullCrossOrigin':
return inject.fullCrossOrigin(domainName, {
+ cspNonce,
modifyObstructiveThirdPartyCode,
modifyObstructiveCode,
simulatedCookies,
@@ -55,6 +59,7 @@ function getHtmlToInject (opts: InjectionOpts & SecurityOpts) {
case 'partial':
return inject.partial(domainName, {
shouldInjectDocumentDomain,
+ cspNonce,
})
default:
return
diff --git a/packages/proxy/lib/types.ts b/packages/proxy/lib/types.ts
index 8c0020ebb6..8343b6640d 100644
--- a/packages/proxy/lib/types.ts
+++ b/packages/proxy/lib/types.ts
@@ -37,6 +37,7 @@ export type CypressWantsInjection = 'full' | 'fullCrossOrigin' | 'partial' | fal
* An outgoing response to an incoming request to the Cypress web server.
*/
export type CypressOutgoingResponse = Response & {
+ injectionNonce?: string
isInitial: null | boolean
wantsInjection: CypressWantsInjection
wantsSecurityRemoved: null | boolean
diff --git a/packages/proxy/test/integration/net-stubbing.spec.ts b/packages/proxy/test/integration/net-stubbing.spec.ts
index 60685ec85f..6437cdb429 100644
--- a/packages/proxy/test/integration/net-stubbing.spec.ts
+++ b/packages/proxy/test/integration/net-stubbing.spec.ts
@@ -28,7 +28,10 @@ context('network stubbing', () => {
let socket
beforeEach((done) => {
- config = {}
+ config = {
+ experimentalCspAllowList: false,
+ }
+
remoteStates = new RemoteStates(() => {})
socket = new EventEmitter()
socket.toDriver = sinon.stub()
@@ -72,9 +75,48 @@ context('network stubbing', () => {
destinationApp.get('/', (req, res) => res.send('it worked'))
+ destinationApp.get('/csp-header-strip', (req, res) => {
+ const headerName = req.query.headerName
+
+ res.setHeader('content-type', 'text/html')
+ res.setHeader(headerName, 'script-src \'self\' localhost')
+ res.send('bar')
+ })
+
+ destinationApp.get('/csp-header-none', (req, res) => {
+ const headerName = req.query.headerName
+
+ proxy.http.config.experimentalCspAllowList = true
+ res.setHeader('content-type', 'text/html')
+ res.setHeader(headerName, 'fake-directive fake-value')
+ res.send('bar')
+ })
+
+ destinationApp.get('/csp-header-single', (req, res) => {
+ const headerName = req.query.headerName
+
+ proxy.http.config.experimentalCspAllowList = ['script-src']
+ res.setHeader('content-type', 'text/html')
+ res.setHeader(headerName, ['default-src \'self\'', 'script-src \'self\' localhost'])
+ res.send('bar')
+ })
+
+ destinationApp.get('/csp-header-multiple', (req, res) => {
+ const headerName = req.query.headerName
+
+ proxy.http.config.experimentalCspAllowList = ['script-src', 'default-src']
+ res.setHeader('content-type', 'text/html')
+ res.setHeader(headerName, ['default-src \'self\'', 'script-src \'self\' localhost'])
+ res.send('bar')
+ })
+
server = allowDestroy(destinationApp.listen(() => {
destinationPort = server.address().port
remoteStates.set(`http://localhost:${destinationPort}`)
+ remoteStates.set(`http://localhost:${destinationPort}/csp-header-strip`)
+ remoteStates.set(`http://localhost:${destinationPort}/csp-header-none`)
+ remoteStates.set(`http://localhost:${destinationPort}/csp-header-single`)
+ remoteStates.set(`http://localhost:${destinationPort}/csp-header-multiple`)
done()
}))
})
@@ -285,4 +327,121 @@ context('network stubbing', () => {
expect(sendContentLength).to.eq(receivedContentLength)
expect(sendContentLength).to.eq(realContentLength)
})
+
+ describe('CSP Headers', () => {
+ // Loop through valid CSP header names can verify that we handle them
+ [
+ 'content-security-policy',
+ 'Content-Security-Policy',
+ 'content-security-policy-report-only',
+ 'Content-Security-Policy-Report-Only',
+ ].forEach((headerName) => {
+ describe(`${headerName}`, () => {
+ it('does not add CSP header if injecting JS and original response had no CSP header', () => {
+ netStubbingState.routes.push({
+ id: '1',
+ routeMatcher: {
+ url: '*',
+ },
+ hasInterceptor: false,
+ staticResponse: {
+ body: 'bar',
+ },
+ getFixture: async () => {},
+ matches: 1,
+ })
+
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}`)
+ .set('Accept', 'text/html,application/xhtml+xml')
+ .then((res) => {
+ expect(res.headers[headerName]).to.be.undefined
+ expect(res.headers[headerName.toLowerCase()]).to.be.undefined
+ })
+ })
+
+ it('removes CSP header by default if not injecting JS and original response had CSP header', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-strip?headerName=${headerName}`)
+ .then((res) => {
+ expect(res.headers[headerName]).to.be.undefined
+ expect(res.headers[headerName.toLowerCase()]).to.be.undefined
+ })
+ })
+
+ it('removes CSP header by default if injecting JS and original response had CSP header', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-strip?headerName=${headerName}`)
+ .then((res) => {
+ expect(res.headers[headerName]).to.be.undefined
+ expect(res.headers[headerName.toLowerCase()]).to.be.undefined
+ })
+ })
+
+ it('does not modify CSP header if not injecting JS and original response had CSP header', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-none?headerName=${headerName}`)
+ .then((res) => {
+ expect(res.headers[headerName.toLowerCase()]).to.equal('fake-directive fake-value')
+ })
+ })
+
+ it('does not modify a CSP header if injecting JS and original response had CSP header, but did not have a directive affecting script-src', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-none?headerName=${headerName}`)
+ .set('Accept', 'text/html,application/xhtml+xml')
+ .then((res) => {
+ expect(res.headers[headerName.toLowerCase()]).to.equal('fake-directive fake-value')
+ })
+ })
+
+ it('modifies a CSP header if injecting JS and original response had CSP header affecting script-src', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-single?headerName=${headerName}`)
+ .set('Accept', 'text/html,application/xhtml+xml')
+ .then((res) => {
+ expect(res.headers[headerName.toLowerCase()]).to.match(/^script-src 'self' localhost 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$/)
+ })
+ })
+
+ it('modifies CSP header if injecting JS and original response had multiple CSP headers and directives', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-multiple?headerName=${headerName}`)
+ .set('Accept', 'text/html,application/xhtml+xml')
+ .then((res) => {
+ expect(res.headers[headerName.toLowerCase()]).to.match(/^default-src 'self' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}', script-src 'self' localhost 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$/)
+ })
+ })
+
+ if (headerName !== headerName.toLowerCase()) {
+ // Do not add a non-lowercase version of a CSP header, because most-restrictive is used
+ it('removes non-lowercase CSP header to avoid conflicts on unmodified CSP headers', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-none?headerName=${headerName}`)
+ .then((res) => {
+ expect(res.headers[headerName]).to.be.undefined
+ })
+ })
+
+ it('removes non-lowercase CSP header to avoid conflicts on modified CSP headers', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-single?headerName=${headerName}`)
+ .set('Accept', 'text/html,application/xhtml+xml')
+ .then((res) => {
+ expect(res.headers[headerName]).to.be.undefined
+ })
+ })
+
+ it('removes non-lowercase CSP header to avoid conflicts on multiple CSP headers', () => {
+ return supertest(app)
+ .get(`/http://localhost:${destinationPort}/csp-header-multiple?headerName=${headerName}`)
+ .set('Accept', 'text/html,application/xhtml+xml')
+ .then((res) => {
+ expect(res.headers[headerName]).to.be.undefined
+ })
+ })
+ }
+ })
+ })
+ })
})
diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts
index efed881d6d..52829fad34 100644
--- a/packages/proxy/test/unit/http/response-middleware.spec.ts
+++ b/packages/proxy/test/unit/http/response-middleware.spec.ts
@@ -7,6 +7,7 @@ import { testMiddleware } from './helpers'
import { RemoteStates } from '@packages/server/lib/remote_states'
import { Readable } from 'stream'
import * as rewriter from '../../../lib/http/util/rewriter'
+import { nonceDirectives, problematicCspDirectives, unsupportedCSPDirectives } from '../../../lib/http/util/csp-header'
describe('http/response-middleware', function () {
it('exports the members in the correct order', function () {
@@ -15,8 +16,8 @@ describe('http/response-middleware', function () {
'AttachPlainTextStreamFn',
'InterceptResponse',
'PatchExpressSetHeader',
- 'SetInjectionLevel',
'OmitProblematicHeaders',
+ 'SetInjectionLevel',
'MaybePreventCaching',
'MaybeStripDocumentDomainFeaturePolicy',
'MaybeCopyCookiesFromIncomingRes',
@@ -187,6 +188,7 @@ describe('http/response-middleware', function () {
ctx = {
res: {
+ getHeaders: () => headers,
set: sinon.stub(),
removeHeader: sinon.stub(),
on: (event, listener) => {},
@@ -199,6 +201,203 @@ describe('http/response-middleware', function () {
}
})
+ describe('OmitProblematicHeaders', function () {
+ const { OmitProblematicHeaders } = ResponseMiddleware
+ let ctx
+
+ [
+ 'set-cookie',
+ 'x-frame-options',
+ 'content-length',
+ 'transfer-encoding',
+ 'connection',
+ ].forEach((prop) => {
+ it(`always removes "${prop}" from incoming headers`, function () {
+ prepareContext({ [prop]: 'foo' })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.set).to.be.calledWith(sinon.match(function (actual) {
+ return actual[prop] === undefined
+ }))
+ })
+ })
+ })
+
+ const validCspHeaderNames = [
+ 'content-security-policy',
+ 'Content-Security-Policy',
+ 'content-security-policy-report-only',
+ 'Content-Security-Policy-Report-Only',
+ ]
+
+ unsupportedCSPDirectives.forEach((directive) => {
+ validCspHeaderNames.forEach((headerName) => {
+ it(`always removes "${directive}" directive from "${headerName}" headers 'when experimentalCspAllowList is true`, () => {
+ prepareContext({
+ [`${headerName}`]: `${directive} 'fake-csp-${directive}-value'; fake-csp-directive fake-csp-value`,
+ }, {
+ experimentalCspAllowList: true,
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ 'fake-csp-directive fake-csp-value',
+ ])
+ })
+ })
+
+ it(`always removes "${directive}" from "${headerName}" headers when experimentalCspAllowList is an empty array`, () => {
+ prepareContext({
+ [`${headerName}`]: `${directive} 'fake-csp-${directive}-value'; fake-csp-directive fake-csp-value`,
+ }, {
+ experimentalCspAllowList: [],
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ 'fake-csp-directive fake-csp-value',
+ ])
+ })
+ })
+
+ it(`always removes "${directive}" from "${headerName}" headers when experimentalCspAllowList is an array including "${directive}"`, () => {
+ prepareContext({
+ [`${headerName}`]: `${directive} 'fake-csp-${directive}-value'; fake-csp-directive fake-csp-value`,
+ }, {
+ experimentalCspAllowList: [`${directive}`],
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ 'fake-csp-directive fake-csp-value',
+ ])
+ })
+ })
+ })
+ })
+
+ validCspHeaderNames.forEach((headerName) => {
+ it(`removes "${headerName}" headers when experimentalCspAllowList is false`, () => {
+ prepareContext({
+ [`${headerName}`]: `fake-csp-directive fake-csp-value`,
+ }, {
+ experimentalCspAllowList: false,
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.removeHeader).to.be.calledWith(headerName.toLowerCase())
+ })
+ })
+ })
+
+ validCspHeaderNames.forEach((headerName) => {
+ it(`will not remove invalid problematicCspDirectives directives provided from "${headerName}" headers when experimentalCspAllowList is an array of directives`, () => {
+ prepareContext({
+ [`${headerName}`]: `fake-csp-directive-0 fake-csp-value-0; fake-csp-directive-1 fake-csp-value-1; fake-csp-directive-2 fake-csp-value-2`,
+ }, {
+ experimentalCspAllowList: ['fake-csp-directive-1'],
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ 'fake-csp-directive-0 fake-csp-value-0; fake-csp-directive-1 fake-csp-value-1; fake-csp-directive-2 fake-csp-value-2',
+ ])
+ })
+ })
+ })
+
+ validCspHeaderNames.forEach((headerName) => {
+ problematicCspDirectives.forEach((directive) => {
+ it(`will allow problematicCspDirectives provided from "${headerName}" headers when experimentalCspAllowList is an array including "${directive}"`, () => {
+ prepareContext({
+ [`${headerName}`]: `fake-csp-directive fake-csp-value; ${directive} fake-csp-${directive}-value`,
+ }, {
+ experimentalCspAllowList: [directive],
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ `fake-csp-directive fake-csp-value; ${directive} fake-csp-${directive}-value`,
+ ])
+ })
+ })
+
+ problematicCspDirectives.forEach((otherDirective) => {
+ if (directive === otherDirective) return
+
+ it(`will still remove other problematicCspDirectives provided from "${headerName}" headers when experimentalCspAllowList is an array including singe directives "${directive}"`, () => {
+ prepareContext({
+ [`${headerName}`]: `${directive} fake-csp-${directive}-value; fake-csp-directive fake-csp-value; ${otherDirective} fake-csp-${otherDirective}-value`,
+ }, {
+ experimentalCspAllowList: [directive],
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ `${directive} fake-csp-${directive}-value; fake-csp-directive fake-csp-value`,
+ ])
+ })
+ })
+
+ it(`will allow both problematicCspDirectives provided from "${headerName}" headers when experimentalCspAllowList is an array including multiple directives ["${directive}","${otherDirective}"]`, () => {
+ prepareContext({
+ [`${headerName}`]: `${directive} fake-csp-${directive}-value; fake-csp-directive fake-csp-value; ${otherDirective} fake-csp-${otherDirective}-value`,
+ }, {
+ experimentalCspAllowList: [directive, otherDirective],
+ })
+
+ return testMiddleware([OmitProblematicHeaders], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(), [
+ `${directive} fake-csp-${directive}-value; fake-csp-directive fake-csp-value; ${otherDirective} fake-csp-${otherDirective}-value`,
+ ])
+ })
+ })
+ })
+ })
+ })
+
+ function prepareContext (additionalHeaders = {}, config = {}) {
+ const headers = {
+ 'content-type': 'text/html',
+ 'content-length': '123',
+ 'content-encoding': 'gzip',
+ 'transfer-encoding': 'chunked',
+ 'set-cookie': 'foo=bar',
+ 'x-frame-options': 'DENY',
+ 'connection': 'keep-alive',
+ }
+
+ ctx = {
+ config: {
+ experimentalCspAllowList: false,
+ ...config,
+ },
+ incomingRes: {
+ headers: {
+ ...headers,
+ ...additionalHeaders,
+ },
+ },
+ res: {
+ removeHeader: sinon.stub(),
+ set: sinon.stub(),
+ setHeader: sinon.stub(),
+ on: (event, listener) => {},
+ off: (event, listener) => {},
+ },
+ }
+ }
+ })
+
describe('SetInjectionLevel', function () {
const { SetInjectionLevel } = ResponseMiddleware
let ctx
@@ -387,6 +586,220 @@ describe('http/response-middleware', function () {
})
})
+ describe('CSP header nonce injection', () => {
+ // Loop through valid CSP header names to verify that we handle them
+ [
+ 'content-security-policy',
+ 'Content-Security-Policy',
+ 'content-security-policy-report-only',
+ 'Content-Security-Policy-Report-Only',
+ ].forEach((headerName) => {
+ describe(`${headerName}`, () => {
+ nonceDirectives.forEach((validNonceDirectiveName) => {
+ it(`modifies existing "${validNonceDirectiveName}" directive for "${headerName}" header if injection is requested, header exists, and "${validNonceDirectiveName}" directive exists`, () => {
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${headerName}`]: `fake-csp-directive fake-csp-value; ${validNonceDirectiveName} \'fake-src\'`,
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(),
+ [sinon.match(new RegExp(`^fake-csp-directive fake-csp-value; ${validNonceDirectiveName} 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$`))])
+ })
+ })
+
+ it(`modifies all existing "${validNonceDirectiveName}" directives for "${headerName}" header if injection is requested, and multiple headers exist with "${validNonceDirectiveName}" directives`, () => {
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${headerName}`]: `fake-csp-directive-0 fake-csp-value-0; ${validNonceDirectiveName} \'fake-src-0\',${validNonceDirectiveName} \'fake-src-1\'`,
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(),
+ [
+ sinon.match(new RegExp(`^fake-csp-directive-0 fake-csp-value-0; ${validNonceDirectiveName} 'fake-src-0' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$`)),
+ sinon.match(new RegExp(`^${validNonceDirectiveName} 'fake-src-1' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$`)),
+ ])
+ })
+ })
+
+ it(`does not modify existing "${validNonceDirectiveName}" directive for "${headerName}" header if injection is not requested`, () => {
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${headerName}`]: `fake-csp-directive fake-csp-value; ${validNonceDirectiveName} \'fake-src\'`,
+ }
+ },
+ wantsInjection: false,
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName, sinon.match.array)
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName.toLowerCase(), sinon.match.array)
+ })
+ })
+
+ it(`does not modify existing "${validNonceDirectiveName}" directive for non-csp headers`, () => {
+ const nonCspHeader = 'Non-Csp-Header'
+
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${nonCspHeader}`]: `${validNonceDirectiveName} \'fake-src\'`,
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).not.to.be.calledWith(nonCspHeader, sinon.match.array)
+ expect(ctx.res.setHeader).not.to.be.calledWith(nonCspHeader.toLowerCase(), sinon.match.array)
+ })
+ })
+
+ nonceDirectives.filter((directive) => directive !== validNonceDirectiveName).forEach((otherNonceDirective) => {
+ it(`modifies existing "${otherNonceDirective}" directive for "${headerName}" header if injection is requested, header exists, and "${validNonceDirectiveName}" directive exists`, () => {
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${headerName}`]: `${validNonceDirectiveName} \'self\'; fake-csp-directive fake-csp-value; ${otherNonceDirective} \'fake-src\'`,
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(),
+ [sinon.match(new RegExp(`^${validNonceDirectiveName} 'self' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; fake-csp-directive fake-csp-value; ${otherNonceDirective} 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$`))])
+ })
+ })
+
+ it(`modifies existing "${otherNonceDirective}" directive for "${headerName}" header if injection is requested, header exists, and "${validNonceDirectiveName}" directive exists in a different header`, () => {
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${headerName}`]: `${validNonceDirectiveName} \'self\',fake-csp-directive fake-csp-value; ${otherNonceDirective} \'fake-src\'`,
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).to.be.calledWith(headerName.toLowerCase(),
+ [
+ sinon.match(new RegExp(`^${validNonceDirectiveName} 'self' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'`)),
+ sinon.match(new RegExp(`^fake-csp-directive fake-csp-value; ${otherNonceDirective} 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'$`)),
+ ])
+ })
+ })
+ })
+ })
+
+ it(`does not append script-src directive in "${headerName}" headers if injection is requested, header exists, but no valid directive exists`, () => {
+ prepareContext({
+ res: {
+ getHeaders () {
+ return {
+ [`${headerName}`]: 'fake-csp-directive fake-csp-value;',
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ // If directive doesn't exist, it shouldn't be updated
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName, sinon.match.array)
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName.toLowerCase(), sinon.match.array)
+ })
+ })
+
+ it(`does not append script-src directive in "${headerName}" headers if injection is requested, and multiple headers exists, but no valid directive exists`, () => {
+ prepareContext({
+ res: {
+ getHeaders: () => {
+ return {
+ [`${headerName}`]: 'fake-csp-directive-0 fake-csp-value-0,fake-csp-directive-1 fake-csp-value-1',
+ }
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ // If directive doesn't exist, it shouldn't be updated
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName, sinon.match.array)
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName.toLowerCase(), sinon.match.array)
+ })
+ })
+
+ it(`does not modify "${headerName}" header if full injection is requested, and header does not exist`, () => {
+ prepareContext({
+ res: {
+ getHeaders: () => {
+ return {}
+ },
+ wantsInjection: 'full',
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName, sinon.match.array)
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName.toLowerCase(), sinon.match.array)
+ })
+ })
+
+ it(`does not modify "${headerName}" header when no injection is requested, and header exists`, () => {
+ prepareContext({
+ res: {
+ getHeaders: () => {
+ return {
+ [`${headerName}`]: 'fake-csp-directive fake-csp-value',
+ }
+ },
+ wantsInjection: false,
+ },
+ })
+
+ return testMiddleware([SetInjectionLevel], ctx)
+ .then(() => {
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName, sinon.match.array)
+ expect(ctx.res.setHeader).not.to.be.calledWith(headerName.toLowerCase(), sinon.match.array)
+ })
+ })
+ })
+ })
+ })
+
describe('wantsSecurityRemoved', () => {
it('removes security if full injection is requested', () => {
prepareContext({
@@ -572,6 +985,9 @@ describe('http/response-middleware', function () {
},
res: {
headers: {},
+ getHeaders: sinon.stub().callsFake(() => {
+ return ctx.res.headers
+ }),
setHeader: sinon.stub(),
on: (event, listener) => {},
off: (event, listener) => {},
@@ -1419,6 +1835,7 @@ describe('http/response-middleware', function () {
.then(() => {
expect(htmlStub).to.be.calledOnce
expect(htmlStub).to.be.calledWith('foo', {
+ 'cspNonce': undefined,
'deferSourceMapRewrite': undefined,
'domainName': 'foobar.com',
'isNotJavascript': true,
@@ -1443,6 +1860,7 @@ describe('http/response-middleware', function () {
.then(() => {
expect(htmlStub).to.be.calledOnce
expect(htmlStub).to.be.calledWith('foo', {
+ 'cspNonce': undefined,
'deferSourceMapRewrite': undefined,
'domainName': '127.0.0.1',
'isNotJavascript': true,
@@ -1475,6 +1893,7 @@ describe('http/response-middleware', function () {
.then(() => {
expect(htmlStub).to.be.calledOnce
expect(htmlStub).to.be.calledWith('foo', {
+ 'cspNonce': undefined,
'deferSourceMapRewrite': undefined,
'domainName': 'foobar.com',
'isNotJavascript': true,
@@ -1490,6 +1909,37 @@ describe('http/response-middleware', function () {
})
})
+ it('cspNonce is set to the value stored in res.injectionNonce', function () {
+ prepareContext({
+ req: {
+ proxiedUrl: 'http://www.foobar.com:3501/primary-origin.html',
+ },
+ res: {
+ injectionNonce: 'fake-nonce',
+ },
+ simulatedCookies: [],
+ })
+
+ return testMiddleware([MaybeInjectHtml], ctx)
+ .then(() => {
+ expect(htmlStub).to.be.calledOnce
+ expect(htmlStub).to.be.calledWith('foo', {
+ 'cspNonce': 'fake-nonce',
+ 'deferSourceMapRewrite': undefined,
+ 'domainName': 'foobar.com',
+ 'isNotJavascript': true,
+ 'modifyObstructiveCode': true,
+ 'modifyObstructiveThirdPartyCode': true,
+ 'shouldInjectDocumentDomain': true,
+ 'url': 'http://www.foobar.com:3501/primary-origin.html',
+ 'useAstSourceRewriting': undefined,
+ 'wantsInjection': 'full',
+ 'wantsSecurityRemoved': true,
+ 'simulatedCookies': [],
+ })
+ })
+ })
+
function prepareContext (props) {
const remoteStates = new RemoteStates(() => {})
const stream = Readable.from(['foo'])
diff --git a/packages/proxy/test/unit/http/util/csp-header.spec.ts b/packages/proxy/test/unit/http/util/csp-header.spec.ts
new file mode 100644
index 0000000000..3c776f2ae0
--- /dev/null
+++ b/packages/proxy/test/unit/http/util/csp-header.spec.ts
@@ -0,0 +1,143 @@
+import { generateCspDirectives, parseCspHeaders } from '../../../../lib/http/util/csp-header'
+
+import { expect } from 'chai'
+
+const patchedHeaders = [
+ 'content-security-policy',
+ 'Content-Security-Policy',
+ 'content-security-policy-report-only',
+ 'Content-Security-Policy-Report-Only',
+]
+
+const cspDirectiveValues = {
+ 'base-uri': ['', ' '],
+ 'block-all-mixed-content': [undefined],
+ 'child-src': ['', ' '],
+ 'connect-src': ['', ' '],
+ 'default-src': ['', ' '],
+ 'font-src': ['', ' '],
+ 'form-action': ['', ' '],
+ 'frame-ancestors': ['\'none\'', '\'self\'', '', ' '],
+ 'frame-src': ['', ' '],
+ 'img-src': ['', ' '],
+ 'manifest-src': ['', ' '],
+ 'media-src': ['', ' '],
+ 'object-src': ['', ' '],
+ 'plugin-types': ['/', '/ /'],
+ 'prefetch-src': ['', ' '],
+ 'referrer': [''],
+ 'report-to': [''],
+ 'report-uri': ['', ' '],
+ 'require-trusted-types-for': ['\'script\''],
+ 'sandbox': [undefined, 'allow-downloads', 'allow-downloads-without-user-activation', 'allow-forms', 'allow-modals', 'allow-orientation-lock', 'allow-pointer-lock', 'allow-popups', 'allow-popups-to-escape-sandbox', 'allow-presentation', 'allow-same-origin', 'allow-scripts', 'allow-storage-access-by-user-activation', 'allow-top-navigation', 'allow-top-navigation-by-user-activation', 'allow-top-navigation-to-custom-protocols'],
+ 'script-src': ['', ' '],
+ 'script-src-attr': ['', ' '],
+ 'script-src-elem': ['', ' '],
+ 'style-src': ['', ' '],
+ 'style-src-attr': ['', ' '],
+ 'style-src-elem': ['', ' '],
+ 'trusted-types': ['none', '', ' \'allow-duplicates\''],
+ 'upgrade-insecure-requests': [undefined],
+ 'worker-src': ['', ' '],
+}
+
+describe('http/util/csp-header', () => {
+ describe('parseCspHeader', () => {
+ patchedHeaders.forEach((headerName) => {
+ it(`should parse a CSP header using "${headerName}"`, () => {
+ const policyArray = parseCspHeaders({
+ 'Content-Type': 'fake-content-type',
+ [`${headerName}`]: 'fake-csp-directive fake-csp-value;',
+ }, headerName)
+
+ expect(policyArray.length).to.equal(1)
+ policyArray.forEach((policyMap) => {
+ expect(policyMap.get('fake-csp-directive')).to.have.members(['fake-csp-value'])
+ }, headerName)
+ })
+
+ it(`should parse a CSP header using multiple "${headerName}" headers`, () => {
+ const policyArray = parseCspHeaders({
+ 'Content-Type': 'fake-content-type',
+ [`${headerName}`]: 'fake-csp-directive-0 fake-csp-value-0,fake-csp-directive-1 fake-csp-value-1',
+ }, headerName)
+
+ expect(policyArray.length).to.equal(2)
+ policyArray.forEach((policyMap, idx) => {
+ expect(policyMap.get(`fake-csp-directive-${idx}`)).to.have.members([`fake-csp-value-${idx}`])
+ }, headerName)
+ })
+
+ it(`should strip a CSP header of all directives specified in the "excludeDirectives" argument for single "${headerName}" headers`, () => {
+ const policyArray = parseCspHeaders({
+ 'Content-Type': 'fake-content-type',
+ [`${headerName}`]: 'fake-csp-directive-0 fake-csp-value-0;fake-csp-directive-1 fake-csp-value-1',
+ }, headerName, ['fake-csp-directive-0'])
+
+ expect(policyArray.length).to.equal(1)
+ policyArray.forEach((policyMap) => {
+ expect(policyMap.has(`fake-csp-directive-0`)).to.equal(false)
+ expect(policyMap.get(`fake-csp-directive-1`)).to.have.members([`fake-csp-value-1`])
+ })
+ })
+
+ it(`should strip a CSP header of all directives specified in the "excludeDirectives" argument for multiple "${headerName}" headers`, () => {
+ const policyArray = parseCspHeaders({
+ 'Content-Type': 'fake-content-type',
+ [`${headerName}`]: 'fake-csp-directive-0 fake-csp-value-0,fake-csp-directive-1 fake-csp-value-1',
+ }, headerName, ['fake-csp-directive-0'])
+
+ expect(policyArray.length).to.equal(2)
+ policyArray.forEach((policyMap, idx) => {
+ if (idx === 0) {
+ expect(policyMap.has(`fake-csp-directive-0`)).to.equal(false)
+ } else {
+ expect(policyMap.get(`fake-csp-directive-1`)).to.have.members([`fake-csp-value-1`])
+ }
+ })
+ })
+
+ describe(`Valid CSP Directives`, () => {
+ Object.entries(cspDirectiveValues).forEach(([directive, values]) => {
+ values.forEach((value) => {
+ it(`should parse a CSP header using "${headerName}" with a valid "${directive}" directive for "${value}"`, () => {
+ const policyArray = parseCspHeaders({
+ 'Content-Type': 'fake-content-type',
+ [`${headerName}`]: `${directive}${value === undefined ? '' : ` ${value}`}`,
+ }, headerName)
+
+ expect(policyArray.length).to.equal(1)
+ policyArray.forEach((policyMap) => {
+ expect(policyMap.has(directive)).to.equal(true)
+ expect(policyMap.get(directive)).to.have.members(value === undefined ? [] : `${value}`.split(' '))
+ }, headerName)
+ })
+
+ it(`should strip a CSP header using "${headerName}" with a valid "${directive}" directive for "${value}" if the directive is excluded`, () => {
+ const policyArray = parseCspHeaders({
+ 'Content-Type': 'fake-content-type',
+ [`${headerName}`]: `${directive}${value === undefined ? '' : ` ${value}`}`,
+ }, headerName, [directive])
+
+ expect(policyArray.length).to.equal(1)
+ policyArray.forEach((policyMap) => {
+ expect(policyMap.has(directive)).to.equal(false)
+ }, headerName)
+ })
+ })
+ })
+ })
+ })
+ })
+
+ describe('generateCspDirectives', () => {
+ it(`should generate a CSP directive string from a policy map`, () => {
+ const policyMap = new Map()
+
+ policyMap.set('fake-csp-directive', ['\'self\'', 'unsafe-inline', 'fake-csp-value'])
+ policyMap.set('default', ['\'self\''])
+
+ expect(generateCspDirectives(policyMap)).equal('fake-csp-directive \'self\' unsafe-inline fake-csp-value; default \'self\'')
+ })
+ })
+})
diff --git a/packages/server/index.d.ts b/packages/server/index.d.ts
index 0d8bada3a7..eb4b7faa28 100644
--- a/packages/server/index.d.ts
+++ b/packages/server/index.d.ts
@@ -19,6 +19,7 @@ export namespace CyServer {
export interface Config {
blockHosts: string | string[]
clientRoute: string
+ experimentalCspAllowList: boolean | Cypress.experimentalCspAllowedDirectives[]
experimentalSourceRewriting: boolean
modifyObstructiveCode: boolean
experimentalModifyObstructiveThirdPartyCode: boolean
diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js
index f42b567b14..257e5510f3 100644
--- a/packages/server/test/integration/http_requests_spec.js
+++ b/packages/server/test/integration/http_requests_spec.js
@@ -34,6 +34,7 @@ const { getRunnerInjectionContents } = require(`@packages/resolve-dist`)
const { createRoutes } = require(`../../lib/routes`)
const { getCtx } = require(`../../lib/makeDataContext`)
const dedent = require('dedent')
+const { unsupportedCSPDirectives } = require('@packages/proxy/lib/http/util/csp-header')
zlib = Promise.promisifyAll(zlib)
@@ -1754,7 +1755,7 @@ describe('Routes', () => {
})
})
- it('omits content-security-policy', function () {
+ it('omits content-security-policy by default', function () {
nock(this.server.remoteStates.current().origin)
.get('/bar')
.reply(200, 'OK', {
@@ -1775,7 +1776,7 @@ describe('Routes', () => {
})
})
- it('omits content-security-policy-report-only', function () {
+ it('omits content-security-policy-report-only by default', function () {
nock(this.server.remoteStates.current().origin)
.get('/bar')
.reply(200, 'OK', {
@@ -1961,6 +1962,326 @@ describe('Routes', () => {
})
})
+ describe('CSP Header', () => {
+ describe('provided', () => {
+ describe('experimentalCspAllowList: false', () => {
+ beforeEach(function () {
+ return this.setup('http://localhost:8080', {
+ config: {
+ experimentalCspAllowList: false,
+ },
+ })
+ })
+
+ it('strips all CSP headers for text/html content-type when "experimentalCspAllowList" is false', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).not.to.have.property('content-security-policy')
+ })
+ })
+ })
+
+ describe('experimentalCspAllowList: true', () => {
+ beforeEach(function () {
+ return this.setup('http://localhost:8080', {
+ config: {
+ experimentalCspAllowList: true,
+ },
+ })
+ })
+
+ it('does not append a "script-src" nonce to CSP header for text/html content-type when no valid directive exists', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'$/)
+ })
+ })
+ })
+
+ describe('experimentalCspAllowList: ["script-src-element", "script-src", "default-src"]', () => {
+ beforeEach(function () {
+ return this.setup('http://localhost:8080', {
+ config: {
+ experimentalCspAllowList: ['script-src-elem', 'script-src', 'default-src'],
+ },
+ })
+ })
+
+ it('appends a nonce to existing CSP header directive "script-src-elem" for text/html content-type when in CSP header', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'; script-src-elem \'fake-src\';',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src-elem 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
+ })
+ })
+
+ it('appends a nonce to existing CSP header directive "script-src" for text/html content-type when in CSP header', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'; script-src \'fake-src\';',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
+ })
+ })
+
+ it('appends a nonce to existing CSP header directive "default-src" for text/html content-type when in CSP header', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'; default-src \'fake-src\';',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; default-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
+ })
+ })
+
+ it('appends a nonce to both CSP header directive "script-src" and "default-src" for text/html content-type when in CSP header when both exist', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'; script-src \'fake-src\'; default-src \'fake-src\';',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; default-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
+ })
+ })
+
+ it('appends a nonce to all valid CSP header directives for text/html content-type when in CSP header', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'; script-src-elem \'fake-src\'; script-src \'fake-src\'; default-src \'fake-src\';',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src-elem 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; script-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; default-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
+ })
+ })
+
+ it('does not remove original CSP header for text/html content-type', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': 'foo \'bar\'',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/foo 'bar'/)
+ })
+ })
+
+ it('does not append a nonce to CSP header if request is not for html', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'application/json',
+ 'content-security-policy': 'foo \'bar\'',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).not.to.match(/script-src 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'/)
+ })
+ })
+
+ it('does not remove original CSP header if request is not for html', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'application/json',
+ 'content-security-policy': 'foo \'bar\'',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'$/)
+ })
+ })
+
+ // The following directives are not supported by Cypress and should be stripped
+ unsupportedCSPDirectives.forEach((directive) => {
+ const headerValue = `${directive} 'none'`
+
+ it(`removes the "${directive}" CSP directive for text/html content-type`, function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ 'content-security-policy': `foo \'bar\'; ${headerValue};`,
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).to.have.property('content-security-policy')
+ expect(res.headers['content-security-policy']).to.match(/^foo 'bar'/)
+ expect(res.headers['content-security-policy']).not.to.match(new RegExp(headerValue))
+ })
+ })
+ })
+ })
+ })
+
+ describe('not provided', () => {
+ it('does not append a nonce to CSP header for text/html content-type', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'text/html',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).not.to.have.property('content-security-policy')
+ })
+ })
+
+ it('does not append a nonce to CSP header if request is not for html', function () {
+ nock(this.server.remoteStates.current().origin)
+ .get('/bar')
+ .reply(200, 'OK', {
+ 'Content-Type': 'application/json',
+ })
+
+ return this.rp({
+ url: 'http://localhost:8080/bar',
+ headers: {
+ 'Cookie': '__cypress.initial=false',
+ },
+ })
+ .then((res) => {
+ expect(res.statusCode).to.eq(200)
+ expect(res.headers).not.to.have.property('content-security-policy')
+ })
+ })
+ })
+ })
+
context('authorization', () => {
it('attaches auth headers when matches origin', function () {
const username = 'u'
diff --git a/packages/server/test/unit/config_spec.js b/packages/server/test/unit/config_spec.js
index d517e3f0ef..5c3fae8fc6 100644
--- a/packages/server/test/unit/config_spec.js
+++ b/packages/server/test/unit/config_spec.js
@@ -572,6 +572,64 @@ describe('lib/config', () => {
})
})
+ context('experimentalCspAllowList', () => {
+ const experimentalCspAllowedDirectives = JSON.stringify(['script-src-elem', 'script-src', 'default-src', 'form-action', 'child-src', 'frame-src']).split(',').join(', ')
+
+ it('passes if false', function () {
+ this.setup({ experimentalCspAllowList: false })
+
+ return this.expectValidationPasses()
+ })
+
+ it('passes if true', function () {
+ this.setup({ experimentalCspAllowList: true })
+
+ return this.expectValidationPasses()
+ })
+
+ it('fails if string', function () {
+ this.setup({ experimentalCspAllowList: 'fake-directive' })
+
+ return this.expectValidationFails(`be an array including any of these values: ${experimentalCspAllowedDirectives}`)
+ })
+
+ it('passes if an empty array', function () {
+ this.setup({ experimentalCspAllowList: [] })
+
+ return this.expectValidationPasses()
+ })
+
+ it('passes if subset of Cypress.experimentalCspAllowedDirectives[]', function () {
+ this.setup({ experimentalCspAllowList: ['default-src', 'form-action'] })
+
+ return this.expectValidationPasses()
+ })
+
+ it('passes if null', function () {
+ this.setup({ experimentalCspAllowList: null })
+
+ return this.expectValidationPasses()
+ })
+
+ it('fails if string[]', function () {
+ this.setup({ experimentalCspAllowList: ['script-src', 'fake-directive-2'] })
+
+ return this.expectValidationFails(`be an array including any of these values: ${experimentalCspAllowedDirectives}`)
+ })
+
+ it('fails if any[]', function () {
+ this.setup({ experimentalCspAllowList: [true, 'default-src'] })
+
+ return this.expectValidationFails(`be an array including any of these values: ${experimentalCspAllowedDirectives}`)
+ })
+
+ it('fails if not falsy, or subset of Cypress.experimentalCspAllowedDirectives[]', function () {
+ this.setup({ experimentalCspAllowList: 1 })
+
+ return this.expectValidationFails(`be an array including any of these values: ${experimentalCspAllowedDirectives}`)
+ })
+ })
+
context('supportFile', () => {
it('passes if false', function () {
this.setup({ e2e: { supportFile: false } })
diff --git a/system-tests/__snapshots__/experimental_csp_allow_list_spec.ts.js b/system-tests/__snapshots__/experimental_csp_allow_list_spec.ts.js
new file mode 100644
index 0000000000..7abd6f330b
--- /dev/null
+++ b/system-tests/__snapshots__/experimental_csp_allow_list_spec.ts.js
@@ -0,0 +1,354 @@
+exports['e2e experimentalCspAllowList / experimentalCspAllowList=[\'script-src-elem\', \'script-src\', \'default-src\', \'form-action\'] / works with [\'script-src-elem\', \'script-src\', \'default-src\'] directives'] = `
+
+====================================================================================================
+
+ (Run Starting)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Cypress: 1.2.3 │
+ │ Browser: FooBrowser 88 │
+ │ Specs: 1 found (with_allow_list_custom.cy.ts) │
+ │ Searched: cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+────────────────────────────────────────────────────────────────────────────────────────────────────
+
+ Running: with_allow_list_custom.cy.ts (1 of 1)
+
+
+ experimentalCspAllowList=['script-src-elem', 'script-src', 'default-src']
+ content-security-policy directive script-src-elem should not be stripped and
+ ✓ allows Cypress to run, including configured inline nonces/hashes
+ ✓ allows Cypress to run, but doesn't allow none configured inline scripts
+ content-security-policy directive script-src should not be stripped and
+ ✓ allows Cypress to run, including configured inline nonces/hashes
+ ✓ allows Cypress to run, but doesn't allow none configured inline scripts
+ content-security-policy directive default-src should not be stripped and
+ ✓ allows Cypress to run, including configured inline nonces/hashes
+ ✓ allows Cypress to run, but doesn't allow none configured inline scripts
+
+
+ 6 passing
+
+
+ (Results)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Tests: 6 │
+ │ Passing: 6 │
+ │ Failing: 0 │
+ │ Pending: 0 │
+ │ Skipped: 0 │
+ │ Screenshots: 0 │
+ │ Video: true │
+ │ Duration: X seconds │
+ │ Spec Ran: with_allow_list_custom.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+ (Video)
+
+ - Video output: /XXX/XXX/XXX/cypress/videos/with_allow_list_custom.cy.ts.mp4
+
+
+====================================================================================================
+
+ (Run Finished)
+
+
+ Spec Tests Passing Failing Pending Skipped
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ ✔ with_allow_list_custom.cy.ts XX:XX 6 6 - - - │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+ ✔ All specs passed! XX:XX 6 6 - - -
+
+
+`
+
+exports['e2e experimentalCspAllowList / experimentalCspAllowList=true / strips out [\'script-src-elem\', \'script-src\', \'default-src\', \'form-action\'] directives'] = `
+
+====================================================================================================
+
+ (Run Starting)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Cypress: 1.2.3 │
+ │ Browser: FooBrowser 88 │
+ │ Specs: 1 found (with_allow_list_true.cy.ts) │
+ │ Searched: cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_true.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+────────────────────────────────────────────────────────────────────────────────────────────────────
+
+ Running: with_allow_list_true.cy.ts (1 of 1)
+
+
+ experimentalCspAllowList=true
+ ✓ passes on inline form action
+ content-security-policy directive script-src-elem should be stripped and
+ ✓ regardless of nonces/hashes
+ content-security-policy directive script-src should be stripped and
+ ✓ regardless of nonces/hashes
+ content-security-policy directive default-src should be stripped and
+ ✓ regardless of nonces/hashes
+
+
+ 4 passing
+
+
+ (Results)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Tests: 4 │
+ │ Passing: 4 │
+ │ Failing: 0 │
+ │ Pending: 0 │
+ │ Skipped: 0 │
+ │ Screenshots: 0 │
+ │ Video: true │
+ │ Duration: X seconds │
+ │ Spec Ran: with_allow_list_true.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+ (Video)
+
+ - Video output: /XXX/XXX/XXX/cypress/videos/with_allow_list_true.cy.ts.mp4
+
+
+====================================================================================================
+
+ (Run Finished)
+
+
+ Spec Tests Passing Failing Pending Skipped
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ ✔ with_allow_list_true.cy.ts XX:XX 4 4 - - - │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+ ✔ All specs passed! XX:XX 4 4 - - -
+
+
+`
+
+exports['e2e experimentalCspAllowList / experimentalCspAllowList=true / always strips known problematic directives and is passive with known working directives'] = `
+
+====================================================================================================
+
+ (Run Starting)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Cypress: 1.2.3 │
+ │ Browser: FooBrowser 88 │
+ │ Specs: 1 found (with_allow_list_custom_or_true.cy.ts) │
+ │ Searched: cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+────────────────────────────────────────────────────────────────────────────────────────────────────
+
+ Running: with_allow_list_custom_or_true.cy.ts (1 of 1)
+
+
+ experimentalCspAllowList is custom or true
+ disallowed
+ ✓ frame-ancestors are always stripped
+ ✓ trusted-types & require-trusted-types-for are always stripped
+ ✓ sandbox is always stripped
+ ✓ navigate-to is always stripped
+ allowed
+ ✓ sample: style-src is not stripped
+ ✓ sample: upgrade-insecure-requests is not stripped
+
+
+ 6 passing
+
+
+ (Results)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Tests: 6 │
+ │ Passing: 6 │
+ │ Failing: 0 │
+ │ Pending: 0 │
+ │ Skipped: 0 │
+ │ Screenshots: 0 │
+ │ Video: true │
+ │ Duration: X seconds │
+ │ Spec Ran: with_allow_list_custom_or_true.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+ (Video)
+
+ - Video output: /XXX/XXX/XXX/cypress/videos/with_allow_list_custom_or_true.cy.ts.mp4
+
+
+====================================================================================================
+
+ (Run Finished)
+
+
+ Spec Tests Passing Failing Pending Skipped
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ ✔ with_allow_list_custom_or_true.cy.t XX:XX 6 6 - - - │
+ │ s │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+ ✔ All specs passed! XX:XX 6 6 - - -
+
+
+`
+
+exports['e2e experimentalCspAllowList / experimentalCspAllowList=[\'script-src-elem\', \'script-src\', \'default-src\', \'form-action\'] / always strips known problematic directives and is passive with known working directives'] = `
+
+====================================================================================================
+
+ (Run Starting)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Cypress: 1.2.3 │
+ │ Browser: FooBrowser 88 │
+ │ Specs: 1 found (with_allow_list_custom_or_true.cy.ts) │
+ │ Searched: cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+────────────────────────────────────────────────────────────────────────────────────────────────────
+
+ Running: with_allow_list_custom_or_true.cy.ts (1 of 1)
+
+
+ experimentalCspAllowList is custom or true
+ disallowed
+ ✓ frame-ancestors are always stripped
+ ✓ trusted-types & require-trusted-types-for are always stripped
+ ✓ sandbox is always stripped
+ ✓ navigate-to is always stripped
+ allowed
+ ✓ sample: style-src is not stripped
+ ✓ sample: upgrade-insecure-requests is not stripped
+
+
+ 6 passing
+
+
+ (Results)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Tests: 6 │
+ │ Passing: 6 │
+ │ Failing: 0 │
+ │ Pending: 0 │
+ │ Skipped: 0 │
+ │ Screenshots: 0 │
+ │ Video: true │
+ │ Duration: X seconds │
+ │ Spec Ran: with_allow_list_custom_or_true.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+ (Video)
+
+ - Video output: /XXX/XXX/XXX/cypress/videos/with_allow_list_custom_or_true.cy.ts.mp4
+
+
+====================================================================================================
+
+ (Run Finished)
+
+
+ Spec Tests Passing Failing Pending Skipped
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ ✔ with_allow_list_custom_or_true.cy.t XX:XX 6 6 - - - │
+ │ s │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+ ✔ All specs passed! XX:XX 6 6 - - -
+
+
+`
+
+exports['e2e experimentalCspAllowList / experimentalCspAllowList=[\'script-src-elem\', \'script-src\', \'default-src\', \'form-action\'] / works with [\'form-action\'] directives'] = `
+
+====================================================================================================
+
+ (Run Starting)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Cypress: 1.2.3 │
+ │ Browser: FooBrowser 88 │
+ │ Specs: 1 found (form_action_with_allow_list_custom.cy.ts) │
+ │ Searched: cypress/e2e/experimental_csp_allow_list_spec/form_action_with_allow_list_custom.cy │
+ │ .ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+────────────────────────────────────────────────────────────────────────────────────────────────────
+
+ Running: form_action_with_allow_list_custom.cy.ts (1 of 1)
+
+
+ experimentalCspAllowList=['script-src-elem', 'script-src', 'default-src']
+ 1) fails on inline form action
+
+
+ 0 passing
+ 1 failing
+
+ 1) experimentalCspAllowList=['script-src-elem', 'script-src', 'default-src']
+ fails on inline form action:
+ CypressError: Timed out after waiting \`1000ms\` for your remote page to load.
+
+Your page did not fire its \`load\` event within \`1000ms\`.
+
+You can try increasing the \`pageLoadTimeout\` value in \`cypress.config.js\` to wait longer.
+
+Browsers will not fire the \`load\` event until all stylesheets and scripts are done downloading.
+
+When this \`load\` event occurs, Cypress will continue running commands.
+ [stack trace lines]
+
+
+
+
+ (Results)
+
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ Tests: 1 │
+ │ Passing: 0 │
+ │ Failing: 1 │
+ │ Pending: 0 │
+ │ Skipped: 0 │
+ │ Screenshots: 1 │
+ │ Video: true │
+ │ Duration: X seconds │
+ │ Spec Ran: form_action_with_allow_list_custom.cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+
+ (Screenshots)
+
+ - /XXX/XXX/XXX/cypress/screenshots/form_action_with_allow_list_custom.cy.ts/experi (1280x720)
+ mentalCspAllowList=['script-src-elem', 'script-src', 'default-src'] -- fails on
+ inline form action (failed).png
+
+
+ (Video)
+
+ - Video output: /XXX/XXX/XXX/cypress/videos/form_action_with_allow_list_custom.cy.ts.mp4
+
+
+====================================================================================================
+
+ (Run Finished)
+
+
+ Spec Tests Passing Failing Pending Skipped
+ ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ ✖ form_action_with_allow_list_custom. XX:XX 1 - 1 - - │
+ │ cy.ts │
+ └────────────────────────────────────────────────────────────────────────────────────────────────┘
+ ✖ 1 of 1 failed (100%) XX:XX 1 - 1 - -
+
+
+`
diff --git a/system-tests/projects/e2e/csp_script_test.html b/system-tests/projects/e2e/csp_script_test.html
new file mode 100644
index 0000000000..95b5cb15d1
--- /dev/null
+++ b/system-tests/projects/e2e/csp_script_test.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ CSP Script Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/form_action_with_allow_list_custom.cy.ts b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/form_action_with_allow_list_custom.cy.ts
new file mode 100644
index 0000000000..cc2336fd1b
--- /dev/null
+++ b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/form_action_with_allow_list_custom.cy.ts
@@ -0,0 +1,19 @@
+describe(`experimentalCspAllowList=['script-src-elem', 'script-src', 'default-src']`, () => {
+ let visitUrl: URL
+ const timeout = 1000
+
+ beforeEach(() => {
+ visitUrl = new URL('http://localhost:4466/csp_script_test.html')
+ })
+
+ it('fails on inline form action', {
+ pageLoadTimeout: timeout,
+ // @ts-expect-error
+ }, () => {
+ visitUrl.searchParams.append('csp', `form-action 'none'`)
+
+ cy.visit(visitUrl.toString())
+
+ cy.get('#submit').click()
+ })
+})
diff --git a/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom.cy.ts b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom.cy.ts
new file mode 100644
index 0000000000..db4d56c106
--- /dev/null
+++ b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom.cy.ts
@@ -0,0 +1,103 @@
+describe(`experimentalCspAllowList=['script-src-elem', 'script-src', 'default-src']`, () => {
+ let cspLogMessages = []
+ let visitUrl: URL
+ let postMessageHandler = ({ data }) => {
+ if (data.event === 'csp-script-ran') {
+ cspLogMessages.push(data.data)
+ }
+ }
+
+ beforeEach(() => {
+ cspLogMessages = []
+ visitUrl = new URL('http://localhost:4466/csp_script_test.html')
+
+ // To test scripts for execution under CSP, we send messages of postMessage to verify a script has run to prevent any cross origin iframe issues
+ window.top.addEventListener('message', postMessageHandler, false)
+ })
+
+ afterEach(() => {
+ window.top.removeEventListener('message', postMessageHandler, false)
+ })
+
+ ;['script-src-elem', 'script-src', 'default-src'].forEach((CSP_directive) => {
+ describe(`content-security-policy directive ${CSP_directive} should not be stripped and`, () => {
+ it(`allows Cypress to run, including configured inline nonces/hashes`, () => {
+ visitUrl.searchParams.append('csp', `${CSP_directive} http://www.foobar.com:4466 http://localhost:4466 'nonce-random_nonce' 'sha256-YM+jfV8mJ3IaF5lqpgvjnYAWdy0k77pupK3tsdMuZv8'`)
+
+ cy.visit(visitUrl.toString())
+
+ // NOTE: for script-src-elem, eval() is allowed to run and is only forbidden if script-src or default-src (as a fallback to script-src) is set.
+ // However, the inline script still needs to have an appropriate hash/nonce in order to execute, hence adding a nonce before adding the script onto the page
+
+ cy.window().then((win) => {
+ try {
+ win.eval(`
+ var script = document.createElement('script');
+ script.textContent = "window.top.postMessage({ event: 'csp-script-ran', data: 'eval script ran'}, '*')";
+ script.nonce = "random_nonce"
+ document.head.appendChild(script);
+ `)
+ } catch (e) {
+ // this fails execution with script-src and default-src as expected. If another condition is met, throw
+ if (CSP_directive === 'script-src-elem') {
+ throw e
+ }
+ }
+ })
+
+ // make sure the stylesheet is loaded with the color purple
+ cy.get('h1').contains('CSP Script Test').should('have.css', 'color', 'rgb(128, 0, 128)')
+
+ // wait a small amount of time for all postMessages to trickle in
+ cy.wait(1000).then(() => {
+ // localhost:4466 and www.foobar.com:4466 script src's are allowed to run
+ expect(cspLogMessages).to.contain('script src origin www.foobar.com:4466 script ran')
+ expect(cspLogMessages).to.contain('script src origin localhost:4466 script ran')
+
+ // since we told the server via query params to let 'random_nonce' and 'sha256-YM+jfV8mJ3IaF5lqpgvjnYAWdy0k77pupK3tsdMuZv8=' inline scripts to execute, these scripts should have executed
+ expect(cspLogMessages).to.contain('nonce script ran')
+
+ // chromium browsers support some features of CSP 3.0, such as hash-source on src like directives
+ // currently, Firefox and Webkit seem to be a bit behind. @see https://www.w3.org/TR/CSP3/
+ if (!['firefox', 'webkit'].includes(Cypress.browser.name)) {
+ expect(cspLogMessages).to.contain('hash script ran')
+ }
+
+ // should have been blocked by CSP as it isn't configured by the server to run
+ expect(cspLogMessages).to.not.contain('script src origin app.foobar.com:4466 script ran')
+
+ // if the src type is script-src-eval, the eval should have ran. Otherwise, it should have been blocked
+ if (CSP_directive === 'script-src-elem') {
+ expect(cspLogMessages).to.contain('eval script ran')
+ } else {
+ expect(cspLogMessages).to.not.contain('eval script ran')
+ }
+ })
+ })
+
+ it(`allows Cypress to run, but doesn't allow none configured inline scripts`, () => {
+ visitUrl.searchParams.append('csp', `${CSP_directive} http://www.foobar.com:4466 http://localhost:4466`)
+
+ cy.visit(visitUrl.toString())
+
+ // make sure the stylesheet is loaded with the color purple
+ cy.get('h1').contains('CSP Script Test').should('have.css', 'color', 'rgb(128, 0, 128)')
+
+ // wait a small amount of time for all postMessages to trickle in
+ cy.wait(1000).then(() => {
+ // localhost:4466 and www.foobar.com:4466 script src's are allowed to run
+ expect(cspLogMessages).to.contain('script src origin www.foobar.com:4466 script ran')
+ expect(cspLogMessages).to.contain('script src origin localhost:4466 script ran')
+
+ // We did not configure any inline script to run, therefore these messages should have never been reported
+ expect(cspLogMessages).to.not.contain('nonce script ran')
+ expect(cspLogMessages).to.not.contain('hash script ran')
+ expect(cspLogMessages).to.not.contain('eval script ran')
+
+ // should have been blocked by CSP as it isn't configured by the server to run
+ expect(cspLogMessages).to.not.contain('script src origin app.foobar.com:4466 script ran')
+ })
+ })
+ })
+ })
+})
diff --git a/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts
new file mode 100644
index 0000000000..b55415c514
--- /dev/null
+++ b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts
@@ -0,0 +1,107 @@
+describe('experimentalCspAllowList is custom or true', () => {
+ let cspLogMessages = []
+ let visitUrl: URL
+ let postMessageHandler = ({ data }) => {
+ if (data.event === 'csp-script-ran') {
+ cspLogMessages.push(data.data)
+ }
+ }
+
+ beforeEach(() => {
+ cspLogMessages = []
+ visitUrl = new URL('http://localhost:4466/csp_script_test.html')
+
+ // To test scripts for execution under CSP, we send messages of postMessage to verify a script has run to prevent any cross origin iframe issues
+ window.top.addEventListener('message', postMessageHandler, false)
+ })
+
+ afterEach(() => {
+ window.top.removeEventListener('message', postMessageHandler, false)
+ })
+
+ describe('disallowed', () => {
+ it('frame-ancestors are always stripped', () => {
+ visitUrl.searchParams.append('csp', `frame-ancestors 'none'`)
+ cy.visit(visitUrl.toString())
+
+ // expect the iframe to load, which implies the csp directive was stripped out
+ cy.get('h1').contains('CSP Script Test').should('be.visible')
+ })
+
+ it('trusted-types & require-trusted-types-for are always stripped', () => {
+ visitUrl.searchParams.append('csp', `require-trusted-types-for 'script'; trusted-types foo bar 'allow-duplicates'`)
+ cy.visit(visitUrl.toString())
+
+ // expect to be able to manipulate the DOM as trusted-types policies are stripped out allowing for injection sink like methods
+ cy.get('h1').its(0).then(($el) => {
+ $el.innerHTML = 'CSP Script Test Modified'
+ })
+
+ cy.get('h1').contains('CSP Script Test Modified').should('be.visible')
+ })
+
+ it('sandbox is always stripped', () => {
+ // Since sandbox is inclusive, all other sandbox actions would be restricted except for `allow-downloads`
+ visitUrl.searchParams.append('csp', `sandbox 'allow-downloads'`)
+ cy.visit(visitUrl.toString())
+
+ // expect the form to post and navigate to a new page, meaning the sandbox directive was stripped
+ cy.get('#submit').click()
+ cy.contains('Cannot POST /').should('exist')
+ })
+
+ it('navigate-to is always stripped', () => {
+ visitUrl.searchParams.append('csp', `navigate-to 'none'`)
+ cy.visit(visitUrl.toString())
+
+ // expect the form to post and navigate to a new page, meaning the navigate-to directive was stripped
+ cy.get('#submit').click()
+ cy.contains('Cannot POST /').should('exist')
+ })
+ })
+
+ describe('allowed', () => {
+ it('sample: style-src is not stripped', () => {
+ visitUrl.searchParams.append('csp', `style-src http://www.foobar.com:4466`)
+ cy.visit(visitUrl.toString())
+
+ // make sure the stylesheet is loaded with the color purple
+ cy.get('h1').contains('CSP Script Test').should('have.css', 'color', 'rgb(128, 0, 128)')
+ })
+
+ it('sample: upgrade-insecure-requests is not stripped', () => {
+ // fake the https automatic upgrade by fulfilling the http request to the express server. verify the requests are actually upraded
+ const requestsFulfilled = {
+ www_foobar_com_script: false,
+ app_foobar_com_script: false,
+ www_foobar_com_style: false,
+ }
+
+ cy.intercept('https://www.foobar.com:4466/csp_empty_script.js', (req) => {
+ requestsFulfilled.www_foobar_com_script = true
+ req.reply('')
+ })
+
+ cy.intercept('https://app.foobar.com:4466/csp_empty_script.js', (req) => {
+ requestsFulfilled.app_foobar_com_script = true
+ req.reply('')
+ })
+
+ cy.intercept('https://www.foobar.com:4466/csp_empty_style.css', (req) => {
+ requestsFulfilled.www_foobar_com_style = true
+ req.reply('')
+ })
+
+ visitUrl.searchParams.append('csp', `upgrade-insecure-requests`)
+ cy.visit(visitUrl.toString())
+
+ cy.get('h1').contains('CSP Script Test').should('be.visible')
+
+ cy.then(() => {
+ Object.keys(requestsFulfilled).forEach((key) => {
+ expect(requestsFulfilled[key]).to.be.true
+ })
+ })
+ })
+ })
+})
diff --git a/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_true.cy.ts b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_true.cy.ts
new file mode 100644
index 0000000000..b9163b6da4
--- /dev/null
+++ b/system-tests/projects/e2e/cypress/e2e/experimental_csp_allow_list_spec/with_allow_list_true.cy.ts
@@ -0,0 +1,73 @@
+describe(`experimentalCspAllowList=true`, () => {
+ let cspLogMessages = []
+ let visitUrl: URL
+ let postMessageHandler = ({ data }) => {
+ if (data.event === 'csp-script-ran') {
+ cspLogMessages.push(data.data)
+ }
+ }
+
+ beforeEach(() => {
+ cspLogMessages = []
+ visitUrl = new URL('http://localhost:4466/csp_script_test.html')
+
+ // To test scripts for execution under CSP, we send messages of postMessage to verify a script has run to prevent any cross origin iframe issues
+ // since ['script-src-elem', 'script-src', 'default-src'] are all stripped out when experimentalCspAllowList=true by default, the messages should always be present
+ window.top.addEventListener('message', postMessageHandler, false)
+ })
+
+ afterEach(() => {
+ window.top.removeEventListener('message', postMessageHandler, false)
+ })
+
+ ;['script-src-elem', 'script-src', 'default-src'].forEach((CSP_directive) => {
+ describe(`content-security-policy directive ${CSP_directive} should be stripped and`, () => {
+ it(` regardless of nonces/hashes`, () => {
+ visitUrl.searchParams.append('csp', `${CSP_directive} http://www.foobar.com:4466 http://localhost:4466 'nonce-random_nonce' 'sha256-YM+jfV8mJ3IaF5lqpgvjnYAWdy0k77pupK3tsdMuZv8'`)
+
+ cy.visit(visitUrl.toString())
+
+ cy.window().then((win) => {
+ return win.eval(`
+ var script = document.createElement('script');
+ script.textContent = "window.top.postMessage({ event: 'csp-script-ran', data: 'eval script ran'}, '*')";
+ script.nonce = "random_nonce"
+ document.head.appendChild(script);
+ `)
+ })
+
+ // make sure the stylesheet is loaded with the color purple
+ cy.get('h1').contains('CSP Script Test').should('have.css', 'color', 'rgb(128, 0, 128)')
+
+ // wait a small amount of time for all postMessages to trickle in
+ cy.wait(1000).then(() => {
+ // since problematic CSP headers are stripped by default, we should have every message from every script
+ expect(cspLogMessages).to.contain('script src origin www.foobar.com:4466 script ran')
+ expect(cspLogMessages).to.contain('script src origin localhost:4466 script ran')
+ expect(cspLogMessages).to.contain('nonce script ran')
+ expect(cspLogMessages).to.contain('hash script ran')
+ expect(cspLogMessages).to.contain('script src origin app.foobar.com:4466 script ran')
+ expect(cspLogMessages).to.contain('eval script ran')
+ })
+ })
+ })
+ })
+
+ const timeout = 1000
+
+ it('passes on inline form action', {
+ pageLoadTimeout: timeout,
+ // @ts-expect-error
+ }, () => {
+ // this should be stripped out in the middleware
+ visitUrl.searchParams.append('csp', `form-action 'none'`)
+
+ cy.visit(visitUrl.toString())
+
+ // expect the form to submit
+ cy.get('#submit').click()
+
+ // expect the form action to go through and NOT be blocked by CSP (even though the action itself fails which is OK)
+ cy.contains('Cannot POST /').should('exist')
+ })
+})
diff --git a/system-tests/projects/e2e/static/csp_styles.css b/system-tests/projects/e2e/static/csp_styles.css
new file mode 100644
index 0000000000..c17c8634b7
--- /dev/null
+++ b/system-tests/projects/e2e/static/csp_styles.css
@@ -0,0 +1,3 @@
+h1{
+ color: purple;
+}
\ No newline at end of file
diff --git a/system-tests/test/experimental_csp_allow_list_spec.ts b/system-tests/test/experimental_csp_allow_list_spec.ts
new file mode 100644
index 0000000000..ebec531dc5
--- /dev/null
+++ b/system-tests/test/experimental_csp_allow_list_spec.ts
@@ -0,0 +1,114 @@
+import path from 'path'
+import systemTests from '../lib/system-tests'
+import Fixtures from '../lib/fixtures'
+
+const e2ePath = Fixtures.projectPath('e2e')
+
+const PORT = 3500
+const onServer = function (app) {
+ app.get(`/csp_empty_style.css`, (req, res) => {
+ // instead of logging, check the color of the h1 inside csp_script_test.html to see if the h1 text color is purple to verify the script ran
+ res.sendFile(path.join(e2ePath, `static/csp_styles.css`))
+ })
+
+ app.get(`/csp_empty_script.js`, (req, res) => {
+ // log the host of the script to postMessage to verify if the script ran or not depending on the test
+ const script = `window.top.postMessage({ event: 'csp-script-ran', data: 'script src origin ${req.get('host')} script ran'}, '*')`
+
+ res.send(script)
+ })
+
+ app.get(`/csp_script_test.html`, (req, res) => {
+ const { csp } = req.query
+
+ res.setHeader('Content-Security-Policy', csp)
+ res.sendFile(path.join(e2ePath, `csp_script_test.html`))
+ })
+}
+
+// NOTE: 'navigate-to' is a CSP 3.0 feature and currently is not shipped with any major browser version. @see https://csplite.com/csp123/.
+describe('e2e experimentalCspAllowList', () => {
+ systemTests.setup({
+ servers: [{
+ port: 4466,
+ onServer,
+ }],
+ settings: {
+ hosts: {
+ '*.foobar.com': '127.0.0.1',
+ },
+ e2e: {},
+ },
+ })
+
+ describe('experimentalCspAllowList=true', () => {
+ systemTests.it('strips out [\'script-src-elem\', \'script-src\', \'default-src\', \'form-action\'] directives', {
+ browser: '!webkit', // TODO(webkit): fix+unskip
+ port: PORT,
+ spec: 'experimental_csp_allow_list_spec/with_allow_list_true.cy.ts',
+ snapshot: true,
+ expectedExitCode: 0,
+ config: {
+ videoCompression: false,
+ retries: 0,
+ experimentalCspAllowList: true,
+ },
+ })
+
+ systemTests.it('always strips known problematic directives and is passive with known working directives', {
+ browser: '!webkit', // TODO(webkit): fix+unskip
+ port: PORT,
+ spec: 'experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts',
+ snapshot: true,
+ expectedExitCode: 0,
+ config: {
+ videoCompression: false,
+ retries: 0,
+ experimentalCspAllowList: true,
+ },
+ })
+ })
+
+ // NOTE: these tests do not 100% work in webkit and are problematic in CI with firefox.
+ describe('experimentalCspAllowList=[\'script-src-elem\', \'script-src\', \'default-src\', \'form-action\']', () => {
+ systemTests.it('works with [\'script-src-elem\', \'script-src\', \'default-src\'] directives', {
+ browser: ['chrome', 'electron'],
+ port: PORT,
+ spec: 'experimental_csp_allow_list_spec/with_allow_list_custom.cy.ts',
+ snapshot: true,
+ expectedExitCode: 0,
+ config: {
+ videoCompression: false,
+ retries: 0,
+ experimentalCspAllowList: ['script-src-elem', 'script-src', 'default-src'],
+ },
+ })
+
+ systemTests.it('always strips known problematic directives and is passive with known working directives', {
+ browser: ['chrome', 'electron'],
+ port: PORT,
+ spec: 'experimental_csp_allow_list_spec/with_allow_list_custom_or_true.cy.ts',
+ snapshot: true,
+ expectedExitCode: 0,
+ config: {
+ videoCompression: false,
+ retries: 0,
+ experimentalCspAllowList: ['script-src-elem', 'script-src', 'default-src', 'form-action'],
+ },
+ })
+
+ systemTests.it('works with [\'form-action\'] directives', {
+ // NOTE: firefox respects on form action, but the submit handler does not trigger a error
+ browser: ['chrome', 'electron'],
+ port: PORT,
+ spec: 'experimental_csp_allow_list_spec/form_action_with_allow_list_custom.cy.ts',
+ snapshot: true,
+ expectedExitCode: 1,
+ config: {
+ videoCompression: false,
+ retries: 0,
+ experimentalCspAllowList: ['form-action'],
+ },
+ })
+ })
+})
From 9cb7e1f915dd74986edc1fbd98e57cdabcbdb6b9 Mon Sep 17 00:00:00 2001
From: Stokes Player
Date: Thu, 15 Jun 2023 20:02:17 -0400
Subject: [PATCH 2/4] feat: Cypress Cloud data on Specs page and Runs page use
local Git data if available (#26991)
---
cli/CHANGELOG.md | 1 +
packages/app/cypress/e2e/debug.cy.ts | 166 +-
packages/app/cypress/e2e/runs.cy.ts | 314 +--
.../app/cypress/e2e/specs_list_flaky.cy.ts | 78 +-
.../cypress/e2e/specs_list_latest_runs.cy.ts | 231 +--
...ql-CloudViewerAndProject_RequiredData.json | 472 -----
.../fixtures/debug-Failing/gql-Debug.json | 8 +-
.../gql-HeaderBar_HeaderBarQuery.json | 666 ------
.../debug-Failing/gql-MainAppQuery.json | 11 -
.../gql-SideBarNavigationContainer.json | 35 -
.../debug-Failing/gql-SpecsPageContainer.json | 819 --------
.../gql-SpecsPageContainer_BranchInfo.json | 10 -
...ql-CloudViewerAndProject_RequiredData.json | 429 ----
.../gql-HeaderBar_HeaderBarQuery.json | 666 ------
.../debug-Passing/gql-MainAppQuery.json | 11 -
.../gql-SideBarNavigationContainer.json | 35 -
.../debug-Passing/gql-SpecsPageContainer.json | 1825 -----------------
.../gql-SpecsPageContainer_BranchInfo.json | 10 -
.../app/src/composables/useCloudSpecData.ts | 1 -
.../app/src/composables/useRelevantRun.ts | 11 +-
packages/app/src/pages/Runs.vue | 33 +-
packages/app/src/pages/Specs/Index.vue | 70 +-
packages/app/src/runs/RunCard.vue | 25 +-
packages/app/src/runs/RunsComposable.ts | 8 +
packages/app/src/runs/RunsContainer.cy.tsx | 70 +-
packages/app/src/runs/RunsContainer.vue | 136 +-
packages/app/src/runs/useGitTreeRuns.ts | 64 +
packages/app/src/runs/useProjectRuns.ts | 128 ++
packages/app/src/specs/AverageDuration.cy.tsx | 2 +-
packages/app/src/specs/AverageDuration.vue | 6 +-
packages/app/src/specs/RunStatusDots.cy.tsx | 8 +-
packages/app/src/specs/RunStatusDots.vue | 65 +-
packages/app/src/specs/SpecsList.cy.tsx | 4 +-
packages/app/src/specs/SpecsList.vue | 3 +-
.../app/src/specs/SpecsListRunWatcher.vue | 50 +
.../specs/flaky-badge/FlakyInformation.cy.tsx | 8 +-
.../specs/flaky-badge/FlakyInformation.vue | 28 +-
.../flaky-badge/FlakySpecSummaryAdapter.vue | 86 -
packages/data-context/src/DataContext.ts | 6 -
.../data-context/src/sources/GitDataSource.ts | 1 +
.../src/sources/RelevantRunSpecsDataSource.ts | 6 +-
.../src/sources/RelevantRunsDataSource.ts | 26 +-
.../src/sources/RemotePollingDataSource.ts | 101 -
packages/data-context/src/sources/index.ts | 1 -
.../sources/RelevantRunsDataSource.spec.ts | 18 +-
packages/graphql/schemas/schema.graphql | 16 +-
.../src/plugins/nexusSlowGuardPlugin.ts | 5 +-
.../schemaTypes/objectTypes/gql-Mutation.ts | 22 -
.../objectTypes/gql-RelevantRun.ts | 9 +
.../objectTypes/gql-Subscription.ts | 16 +-
packages/graphql/test/stubCloudTypes.ts | 22 +-
51 files changed, 944 insertions(+), 5898 deletions(-)
delete mode 100644 packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json
delete mode 100644 packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json
delete mode 100644 packages/app/cypress/fixtures/debug-Failing/gql-MainAppQuery.json
delete mode 100644 packages/app/cypress/fixtures/debug-Failing/gql-SideBarNavigationContainer.json
delete mode 100644 packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json
delete mode 100644 packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer_BranchInfo.json
delete mode 100644 packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json
delete mode 100644 packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json
delete mode 100644 packages/app/cypress/fixtures/debug-Passing/gql-MainAppQuery.json
delete mode 100644 packages/app/cypress/fixtures/debug-Passing/gql-SideBarNavigationContainer.json
delete mode 100644 packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json
delete mode 100644 packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer_BranchInfo.json
create mode 100644 packages/app/src/runs/RunsComposable.ts
create mode 100644 packages/app/src/runs/useGitTreeRuns.ts
create mode 100644 packages/app/src/runs/useProjectRuns.ts
create mode 100644 packages/app/src/specs/SpecsListRunWatcher.vue
delete mode 100644 packages/app/src/specs/flaky-badge/FlakySpecSummaryAdapter.vue
delete mode 100644 packages/data-context/src/sources/RemotePollingDataSource.ts
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 31b72ee2ac..cfc8e01bf0 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -8,6 +8,7 @@ _Released 06/20/2023 (PENDING)_
- 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:**
diff --git a/packages/app/cypress/e2e/debug.cy.ts b/packages/app/cypress/e2e/debug.cy.ts
index a6efd30ff4..4ffd130cda 100644
--- a/packages/app/cypress/e2e/debug.cy.ts
+++ b/packages/app/cypress/e2e/debug.cy.ts
@@ -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')
})
})
})
diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts
index 30c1d5e69f..f53d861efd 100644
--- a/packages/app/cypress/e2e/runs.cy.ts
+++ b/packages/app/cypress/e2e/runs.cy.ts
@@ -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')
- })
})
})
diff --git a/packages/app/cypress/e2e/specs_list_flaky.cy.ts b/packages/app/cypress/e2e/specs_list_flaky.cy.ts
index b3c1b34285..8c2d97e49c 100644
--- a/packages/app/cypress/e2e/specs_list_flaky.cy.ts
+++ b/packages/app/cypress/e2e/specs_list_flaky.cy.ts
@@ -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,
}
}
diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
index 7f46173ff5..caf3a8fc92 100644
--- a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
+++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
@@ -48,7 +48,7 @@ function specShouldShow (specFileName: string, runDotsClasses: string[], latestR
const latestStatusSpinning = latestRunStatus === 'RUNNING'
type dotIndex = Parameters[1];
- const indexes: dotIndex[] = [0, 1, 2]
+ const indexes: Exclude[] = [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 }, () => {
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json b/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json
deleted file mode 100644
index df9bf17111..0000000000
--- a/packages/app/cypress/fixtures/debug-Failing/gql-CloudViewerAndProject_RequiredData.json
+++ /dev/null
@@ -1,472 +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": "experimentalCspAllowList"
- },
- {
- "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": true,
- "from": "default",
- "field": "video"
- },
- {
- "value": 32,
- "from": "default",
- "field": "videoCompression"
- },
- {
- "value": "cypress/videos",
- "from": "default",
- "field": "videosFolder"
- },
- {
- "value": true,
- "from": "default",
- "field": "videoUploadOnPasses"
- },
- {
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json b/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json
index 576baa8a4a..419c5f2adc 100644
--- a/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json
+++ b/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json
@@ -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=="
],
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json b/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json
deleted file mode 100644
index c9c094137b..0000000000
--- a/packages/app/cypress/fixtures/debug-Failing/gql-HeaderBar_HeaderBarQuery.json
+++ /dev/null
@@ -1,666 +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": "experimentalCspAllowList"
- },
- {
- "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": true,
- "from": "default",
- "field": "video"
- },
- {
- "value": 32,
- "from": "default",
- "field": "videoCompression"
- },
- {
- "value": "cypress/videos",
- "from": "default",
- "field": "videosFolder"
- },
- {
- "value": true,
- "from": "default",
- "field": "videoUploadOnPasses"
- },
- {
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-MainAppQuery.json b/packages/app/cypress/fixtures/debug-Failing/gql-MainAppQuery.json
deleted file mode 100644
index ef32a1eb91..0000000000
--- a/packages/app/cypress/fixtures/debug-Failing/gql-MainAppQuery.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "data": {
- "baseError": null,
- "currentProject": {
- "id": "debug-test-project-id",
- "isLoadingConfigFile": false,
- "isLoadingNodeEvents": false,
- "__typename": "CurrentProject"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-SideBarNavigationContainer.json b/packages/app/cypress/fixtures/debug-Failing/gql-SideBarNavigationContainer.json
deleted file mode 100644
index f5e4672836..0000000000
--- a/packages/app/cypress/fixtures/debug-Failing/gql-SideBarNavigationContainer.json
+++ /dev/null
@@ -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
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json b/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json
deleted file mode 100644
index 9ffbedf671..0000000000
--- a/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer.json
+++ /dev/null
@@ -1,819 +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": "experimentalCspAllowList"
- },
- {
- "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": true,
- "from": "default",
- "field": "video"
- },
- {
- "value": 32,
- "from": "default",
- "field": "videoCompression"
- },
- {
- "value": "cypress/videos",
- "from": "default",
- "field": "videosFolder"
- },
- {
- "value": true,
- "from": "default",
- "field": "videoUploadOnPasses"
- },
- {
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer_BranchInfo.json b/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer_BranchInfo.json
deleted file mode 100644
index d3c582336d..0000000000
--- a/packages/app/cypress/fixtures/debug-Failing/gql-SpecsPageContainer_BranchInfo.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "data": {
- "currentProject": {
- "id": "debug-test-project-id",
- "branch": "main",
- "projectId": "vgqrwp",
- "__typename": "CurrentProject"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json b/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json
deleted file mode 100644
index 1f982a1f7a..0000000000
--- a/packages/app/cypress/fixtures/debug-Passing/gql-CloudViewerAndProject_RequiredData.json
+++ /dev/null
@@ -1,429 +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": "experimentalCspAllowList"
- },
- {
- "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": true,
- "from": "default",
- "field": "video"
- },
- {
- "value": 32,
- "from": "default",
- "field": "videoCompression"
- },
- {
- "value": "cypress/videos",
- "from": "default",
- "field": "videosFolder"
- },
- {
- "value": true,
- "from": "default",
- "field": "videoUploadOnPasses"
- },
- {
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json b/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json
deleted file mode 100644
index c9c094137b..0000000000
--- a/packages/app/cypress/fixtures/debug-Passing/gql-HeaderBar_HeaderBarQuery.json
+++ /dev/null
@@ -1,666 +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": "experimentalCspAllowList"
- },
- {
- "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": true,
- "from": "default",
- "field": "video"
- },
- {
- "value": 32,
- "from": "default",
- "field": "videoCompression"
- },
- {
- "value": "cypress/videos",
- "from": "default",
- "field": "videosFolder"
- },
- {
- "value": true,
- "from": "default",
- "field": "videoUploadOnPasses"
- },
- {
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-MainAppQuery.json b/packages/app/cypress/fixtures/debug-Passing/gql-MainAppQuery.json
deleted file mode 100644
index 5e37c261c6..0000000000
--- a/packages/app/cypress/fixtures/debug-Passing/gql-MainAppQuery.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "data": {
- "baseError": null,
- "currentProject": {
- "id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
- "isLoadingConfigFile": false,
- "isLoadingNodeEvents": false,
- "__typename": "CurrentProject"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-SideBarNavigationContainer.json b/packages/app/cypress/fixtures/debug-Passing/gql-SideBarNavigationContainer.json
deleted file mode 100644
index ea6cd32793..0000000000
--- a/packages/app/cypress/fixtures/debug-Passing/gql-SideBarNavigationContainer.json
+++ /dev/null
@@ -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
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json b/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json
deleted file mode 100644
index 81ff604e86..0000000000
--- a/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer.json
+++ /dev/null
@@ -1,1825 +0,0 @@
-{
- "data": {
- "currentProject": {
- "id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
- "projectRoot": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend",
- "currentTestingType": "component",
- "cloudProject": {
- "__typename": "CloudProject",
- "id": "Q2xvdWRQcm9qZWN0OjdwNXVjZQ=="
- },
- "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": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5RWFXWm1hV04xYkhSNVNYUmxiUzVqZVM1MGN5Sjk=",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZSR2xtWm1samRXeDBlVWwwWlcwdVkza3VkSE09",
- "retrievedAt": "2023-01-30T08:27:41.958Z",
- "averageDuration": 143.5,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpaalU0TnpJeFltSXROek13T1Mxa05XWXlMV05pT1dNdE5UVTRZemRsTVdKak9HUTE=",
- "runNumber": 2,
- "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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 163,
- "max": 163,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22417c1c76-266a-4919-90c3-80bd7c54a079%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyItem.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- },
- {
- "id": "Q2xvdWRTcGVjUnVuOmUyYTdjYWQ2LWU1NDgtNDdjMy05YTI0LWIyMTQ5MWM0NDViMzpaalU0TnpJeFltSXROek13T1Mxa05XWXlMV05pT1dNdE5UVTRZemRsTVdKak9HUTE=",
- "runNumber": 1,
- "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-30T06:46:21.899Z",
- "groupCount": 1,
- "specDuration": {
- "min": 124,
- "max": 124,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/1/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22a6f04b21-1469-4c83-8eab-3bf3e27bd5d9%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": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9EaWZmaWN1bHR5TGFiZWwuY3kudHM=",
- "name": "src/components/DifficultyLabel.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/DifficultyLabel.cy.ts",
- "baseName": "DifficultyLabel.cy.ts",
- "fileName": "DifficultyLabel",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/DifficultyLabel.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-11-11 17:54:50 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "c878ac4",
- "subject": "chore: fix build",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5RWFXWm1hV04xYkhSNVRHRmlaV3d1WTNrdWRITWlmUT09",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZSR2xtWm1samRXeDBlVXhoWW1Wc0xtTjVMblJ6",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 82,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpZV1ExTmpCalptUXRNRE16WXkweE1HVXpMV0l3WXpNdFlqQXhaVGswTnpZMU5qVTQ=",
- "runNumber": 2,
- "basename": "DifficultyLabel.cy.ts",
- "path": "src/components/DifficultyLabel.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 82,
- "max": 82,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%2270ca1044-12a0-4d2e-8f19-09fe8b204a33%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyLabel.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9EaWZmaWN1bHR5UGFuZWwuY3kudHM=",
- "name": "src/components/DifficultyPanel.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/DifficultyPanel.cy.ts",
- "baseName": "DifficultyPanel.cy.ts",
- "fileName": "DifficultyPanel",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/DifficultyPanel.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-11-11 11:49:31 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "3d3969b",
- "subject": "wip: animation (#42)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5RWFXWm1hV04xYkhSNVVHRnVaV3d1WTNrdWRITWlmUT09",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZSR2xtWm1samRXeDBlVkJoYm1Wc0xtTjVMblJ6",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 63,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpORGM0WkRBMVpXWXRPRFJqTWkxa1pEQXdMVE0yWlRJdE9EYzVNREV6WmpSa05XVmo=",
- "runNumber": 2,
- "basename": "DifficultyPanel.cy.ts",
- "path": "src/components/DifficultyPanel.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 63,
- "max": 63,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22e4a26572-8cc4-4748-b46b-6227a54d5efe%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FDifficultyPanel.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9QbGF5U3ltYm9sLmN5LnRz",
- "name": "src/components/PlaySymbol.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/PlaySymbol.cy.ts",
- "baseName": "PlaySymbol.cy.ts",
- "fileName": "PlaySymbol",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/PlaySymbol.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": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5UWJHRjVVM2x0WW05c0xtTjVMblJ6SW4wPQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVR3hoZVZONWJXSnZiQzVqZVM1MGN3PT0=",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 86,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpOakJtTkdObU9URXRORFF4WkMwek5qRmlMV0V3TTJFdE5qazJZVFpqWldZelpUa3g=",
- "runNumber": 2,
- "basename": "PlaySymbol.cy.ts",
- "path": "src/components/PlaySymbol.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 86,
- "max": 86,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%225d57972d-7c46-4d6a-8ed0-6b3758fbe601%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FPlaySymbol.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9Tb25nSW5mby5jeS50c3g=",
- "name": "src/components/SongInfo.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/SongInfo.cy.tsx",
- "baseName": "SongInfo.cy.tsx",
- "fileName": "SongInfo",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/components/SongInfo.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-10-17 15:36:42 +1000",
- "lastModifiedHumanReadable": "4 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "8d26364",
- "subject": "feat: redesign non gameplay screens (#26)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5VGIyNW5TVzVtYnk1amVTNTBjM2dpZlE9PQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVMjl1WjBsdVptOHVZM2t1ZEhONA==",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 150,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpOelpqTmpnMk5XTXRNVFJrWkMxa1pHSmxMVFU0T0dZdE5qSTVOMkV4WmpnM01tVXc=",
- "runNumber": 2,
- "basename": "SongInfo.cy.tsx",
- "path": "src/components/SongInfo.cy.tsx",
- "extension": ".cy.tsx",
- "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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 150,
- "max": 150,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22ea0a04b3-a565-4a38-b628-c83ea1dd5fae%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FSongInfo.cy.tsx%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9Tb25nVGlsZS5jeS50c3g=",
- "name": "src/components/SongTile.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/SongTile.cy.tsx",
- "baseName": "SongTile.cy.tsx",
- "fileName": "SongTile",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/components/SongTile.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-12-01 22:21:22 +1000",
- "lastModifiedHumanReadable": "9 weeks ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "2617bc6",
- "subject": "feat: overlay while images load to avoid layout repaint (#46)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5VGIyNW5WR2xzWlM1amVTNTBjM2dpZlE9PQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVMjl1WjFScGJHVXVZM2t1ZEhONA==",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 42,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpabUUyT1RBMk16VXRZVEJoTnkxaFpUSm1MV1JpWm1NdE1HTXdObU5qWkRCaVkyVmg=",
- "runNumber": 2,
- "basename": "SongTile.cy.tsx",
- "path": "src/components/SongTile.cy.tsx",
- "extension": ".cy.tsx",
- "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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 42,
- "max": 42,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%2296490e8f-cc33-4fdc-96ef-3b33fd0b62cc%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FSongTile.cy.tsx%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9Nb2RpZmllclBhbmVsL01vZGlmaWVyUGFuZWwuY3kudHM=",
- "name": "src/components/ModifierPanel/ModifierPanel.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/ModifierPanel/ModifierPanel.cy.ts",
- "baseName": "ModifierPanel.cy.ts",
- "fileName": "ModifierPanel",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/ModifierPanel/ModifierPanel.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": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5TmIyUnBabWxsY2xCaGJtVnNMMDF2WkdsbWFXVnlVR0Z1Wld3dVkza3VkSE1pZlE9PQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZUVzlrYVdacFpYSlFZVzVsYkM5TmIyUnBabWxsY2xCaGJtVnNMbU41TG5Seg==",
- "retrievedAt": "2023-01-30T08:27:41.962Z",
- "averageDuration": 343,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpOemRtT1RGaU9XSXRPREJrTmkwM05tWmpMVE14T0RRdFlXUXlZVE13T1RGalpXRTA=",
- "runNumber": 2,
- "basename": "ModifierPanel.cy.ts",
- "path": "src/components/ModifierPanel/ModifierPanel.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 343,
- "max": 343,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22424504f8-bb4b-4c60-89de-06fefe74fc94%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FModifierPanel%2FModifierPanel.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9JbnB1dC9JbnB1dC5jeS50cw==",
- "name": "src/components/Input/Input.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/Input/Input.cy.ts",
- "baseName": "Input.cy.ts",
- "fileName": "Input",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/Input/Input.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-09-14 17:48:14 +1000",
- "lastModifiedHumanReadable": "5 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "b0f0bd8",
- "subject": "wip: authentication (#19)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5SmJuQjFkQzlKYm5CMWRDNWplUzUwY3lKOQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZTVzV3ZFhRdlNXNXdkWFF1WTNrdWRITT0=",
- "retrievedAt": "2023-01-30T08:27:41.962Z",
- "averageDuration": 134,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpZall4TWpFeE9Ua3RZMlkzWVMxaVpEUTVMV016TUdNdE9HRTVaVFV4Tnpsa09EZG0=",
- "runNumber": 2,
- "basename": "Input.cy.ts",
- "path": "src/components/Input/Input.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 134,
- "max": 134,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22b792a8db-ef62-4e58-b0cb-48539857e6e7%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FInput%2FInput.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": "2022-09-14 17:48:14 +1000",
- "lastModifiedHumanReadable": "5 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "b0f0bd8",
- "subject": "wip: authentication (#19)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5SmJtWnZVR0Z1Wld3dlNXNW1iMUJoYm1Wc0xtTjVMblJ6SW4wPQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZTVzVtYjFCaGJtVnNMMGx1Wm05UVlXNWxiQzVqZVM1MGN3PT0=",
- "retrievedAt": "2023-01-30T08:27:41.961Z",
- "averageDuration": 2293.5,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
- "runNumber": 2,
- "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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 56,
- "max": 56,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%222bfc18aa-1f0b-4c98-801e-5128fa1d89ed%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FInfoPanel%2FInfoPanel.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- },
- {
- "id": "Q2xvdWRTcGVjUnVuOmUyYTdjYWQ2LWU1NDgtNDdjMy05YTI0LWIyMTQ5MWM0NDViMzpNekExTlRVNE1UWXRNalZqTmkxak0yWmlMVEU0WWpFdFkyWTVaV1JrWkRFM05qTmk=",
- "runNumber": 1,
- "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-30T06:46:21.899Z",
- "groupCount": 1,
- "specDuration": {
- "min": 4531,
- "max": 4531,
- "__typename": "SpecDataAggregate"
- },
- "status": "FAILED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/1/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22e4130238-90c7-4331-9ab8-308c101c1095%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"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9Ob25HYW1lcGxheVNjcmVlbi9Ob25HYW1lcGxheVNjcmVlbi5jeS50cw==",
- "name": "src/components/NonGameplayScreen/NonGameplayScreen.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/NonGameplayScreen/NonGameplayScreen.cy.ts",
- "baseName": "NonGameplayScreen.cy.ts",
- "fileName": "NonGameplayScreen",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/NonGameplayScreen/NonGameplayScreen.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2023-01-29 17:06:20 +1000",
- "lastModifiedHumanReadable": "25 hours ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "37fa5bf",
- "subject": "chore: fix types",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5T2IyNUhZVzFsY0d4aGVWTmpjbVZsYmk5T2IyNUhZVzFsY0d4aGVWTmpjbVZsYmk1amVTNTBjeUo5",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZUbTl1UjJGdFpYQnNZWGxUWTNKbFpXNHZUbTl1UjJGdFpYQnNZWGxUWTNKbFpXNHVZM2t1ZEhNPQ==",
- "retrievedAt": "2023-01-30T08:27:41.962Z",
- "averageDuration": 156,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpaVFF6TURSak1EY3RNekZqWkMxaE1qWTJMV1UwTVRFdE1tTTBObVUyWldRMU9USXc=",
- "runNumber": 2,
- "basename": "NonGameplayScreen.cy.ts",
- "path": "src/components/NonGameplayScreen/NonGameplayScreen.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 156,
- "max": 156,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%2228cc2158-81b5-4974-b16f-cc31d9b04743%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FNonGameplayScreen%2FNonGameplayScreen.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9QbGFpblBhbmVsL1BsYWluUGFuZWwuY3kudHM=",
- "name": "src/components/PlainPanel/PlainPanel.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/PlainPanel/PlainPanel.cy.ts",
- "baseName": "PlainPanel.cy.ts",
- "fileName": "PlainPanel",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/PlainPanel/PlainPanel.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-09-14 17:48:14 +1000",
- "lastModifiedHumanReadable": "5 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "b0f0bd8",
- "subject": "wip: authentication (#19)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5UWJHRnBibEJoYm1Wc0wxQnNZV2x1VUdGdVpXd3VZM2t1ZEhNaWZRPT0=",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVR3hoYVc1UVlXNWxiQzlRYkdGcGJsQmhibVZzTG1ONUxuUno=",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 84,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpPV1F4TlRoak1UZ3RaamhpTWkwd1ptSm1MVGMyTTJVdFptSmhNemd5WkdRM05XRTE=",
- "runNumber": 2,
- "basename": "PlainPanel.cy.ts",
- "path": "src/components/PlainPanel/PlainPanel.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 84,
- "max": 84,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22d6a84337-f91a-4ac1-aa99-44258686a8db%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FPlainPanel%2FPlainPanel.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9TY29yZUJhZGdlL1Njb3JlQmFkZ2UuY3kudHM=",
- "name": "src/components/ScoreBadge/ScoreBadge.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/ScoreBadge/ScoreBadge.cy.ts",
- "baseName": "ScoreBadge.cy.ts",
- "fileName": "ScoreBadge",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/ScoreBadge/ScoreBadge.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": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5VFkyOXlaVUpoWkdkbEwxTmpiM0psUW1Ga1oyVXVZM2t1ZEhNaWZRPT0=",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVMk52Y21WQ1lXUm5aUzlUWTI5eVpVSmhaR2RsTG1ONUxuUno=",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 36,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpPVGswT1dNM09HTXRZams1WmkwM01XWTFMV1JoTjJRdFlqYzBaRE5tWVRkbFpUUTI=",
- "runNumber": 2,
- "basename": "ScoreBadge.cy.ts",
- "path": "src/components/ScoreBadge/ScoreBadge.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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 36,
- "max": 36,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%2268d839b4-003c-4fd2-af26-736c429c71aa%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FScoreBadge%2FScoreBadge.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9TaWduSW5Gb3JtL1NpZ25JbkZvcm0uY3kudHN4",
- "name": "src/components/SignInForm/SignInForm.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/SignInForm/SignInForm.cy.tsx",
- "baseName": "SignInForm.cy.tsx",
- "fileName": "SignInForm",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/components/SignInForm/SignInForm.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-10-20 21:42:48 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "54ed885",
- "subject": "feat: track and display scores (#31)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5VGFXZHVTVzVHYjNKdEwxTnBaMjVKYmtadmNtMHVZM2t1ZEhONEluMD0=",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVMmxuYmtsdVJtOXliUzlUYVdkdVNXNUdiM0p0TG1ONUxuUnplQT09",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 85,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpZamMxWlRFeE1HTXRNREl6T1MxaFpUaGtMVEF4WmpjdFpUQmtZelkxWkRrM09UY3g=",
- "runNumber": 2,
- "basename": "SignInForm.cy.tsx",
- "path": "src/components/SignInForm/SignInForm.cy.tsx",
- "extension": ".cy.tsx",
- "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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 85,
- "max": 85,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22f0d81de4-5455-459a-a0aa-654ec07ffe89%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FSignInForm%2FSignInForm.cy.tsx%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvY29tcG9uZW50cy9TaWduVXBGb3JtL1NpZ25VcEZvcm0uY3kudHM=",
- "name": "src/components/SignUpForm/SignUpForm.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/components/SignUpForm/SignUpForm.cy.ts",
- "baseName": "SignUpForm.cy.ts",
- "fileName": "SignUpForm",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/components/SignUpForm/SignUpForm.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-09-14 17:48:14 +1000",
- "lastModifiedHumanReadable": "5 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "b0f0bd8",
- "subject": "wip: authentication (#19)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12WTI5dGNHOXVaVzUwY3k5VGFXZHVWWEJHYjNKdEwxTnBaMjVWY0VadmNtMHVZM2t1ZEhNaWZRPT0=",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwyTnZiWEJ2Ym1WdWRITXZVMmxuYmxWd1JtOXliUzlUYVdkdVZYQkdiM0p0TG1ONUxuUno=",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 37,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpOMll3TWpFMk5tVXRPV1kxTkMwd09UY3pMVE16TWpndE1EY3dObVptTnpnMk1XUTU=",
- "runNumber": 2,
- "basename": "SignUpForm.cy.ts",
- "path": "src/components/SignUpForm/SignUpForm.cy.ts",
- "extension": ".cy.ts",
- "testsFailed": {
- "min": 0,
- "max": 0,
- "__typename": "SpecDataAggregate"
- },
- "testsPassed": {
- "min": 0,
- "max": 0,
- "__typename": "SpecDataAggregate"
- },
- "testsPending": {
- "min": 1,
- "max": 1,
- "__typename": "SpecDataAggregate"
- },
- "testsSkipped": {
- "min": 0,
- "max": 0,
- "__typename": "SpecDataAggregate"
- },
- "createdAt": "2023-01-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 37,
- "max": 37,
- "__typename": "SpecDataAggregate"
- },
- "status": "NOTESTS",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%2274934d6b-ae18-4e0f-84c2-638b3e80acb5%5C%22%5D%22%2C%22label%22%3A%22src%2Fcomponents%2FSignUpForm%2FSignUpForm.cy.ts%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvc2NyZWVucy9Tb25nU2VsZWN0U2NyZWVuL0xvYWRpbmdTY3JlZW4uY3kudHN4",
- "name": "src/screens/SongSelectScreen/LoadingScreen.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/screens/SongSelectScreen/LoadingScreen.cy.tsx",
- "baseName": "LoadingScreen.cy.tsx",
- "fileName": "LoadingScreen",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/screens/SongSelectScreen/LoadingScreen.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-12-01 22:21:22 +1000",
- "lastModifiedHumanReadable": "9 weeks ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "2617bc6",
- "subject": "feat: overlay while images load to avoid layout repaint (#46)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12YzJOeVpXVnVjeTlUYjI1blUyVnNaV04wVTJOeVpXVnVMMHh2WVdScGJtZFRZM0psWlc0dVkza3VkSE40SW4wPQ==",
- "fetchingStatus": "FETCHED",
- "data": {
- "__typename": "CloudProjectSpec",
- "id": "Q2xvdWRQcm9qZWN0U3BlYzo3cDV1Y2U6YzNKakwzTmpjbVZsYm5NdlUyOXVaMU5sYkdWamRGTmpjbVZsYmk5TWIyRmthVzVuVTJOeVpXVnVMbU41TG5SemVBPT0=",
- "retrievedAt": "2023-01-30T08:27:41.963Z",
- "averageDuration": 108,
- "isConsideredFlaky": false,
- "flakyStatus": {
- "__typename": "CloudFeatureNotEnabled"
- },
- "specRuns": {
- "nodes": [
- {
- "id": "Q2xvdWRTcGVjUnVuOjBjOGM2NzJlLTQ4MjItNGIyYS05MjVhLTc1MDhlMmM0MmRiZjpNV1JqWW1FMVpXTXRNVEUwTUMxallqY3dMVEppTkRndE9HWXhObUV5TnpVeVpqRXc=",
- "runNumber": 2,
- "basename": "LoadingScreen.cy.tsx",
- "path": "src/screens/SongSelectScreen/LoadingScreen.cy.tsx",
- "extension": ".cy.tsx",
- "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-30T08:10:59.720Z",
- "groupCount": 1,
- "specDuration": {
- "min": 108,
- "max": 108,
- "__typename": "SpecDataAggregate"
- },
- "status": "PASSED",
- "url": "https://cloud.cypress.io/projects/7p5uce/runs/2/test-results?specs=%5B%7B%22value%22%3A%22%5B%5C%22da1c0015-ebfe-4be1-8da1-f56c5a09ba26%5C%22%5D%22%2C%22label%22%3A%22src%2Fscreens%2FSongSelectScreen%2FLoadingScreen.cy.tsx%22%7D%5D",
- "__typename": "CloudSpecRun"
- }
- ],
- "__typename": "CloudSpecRunConnection"
- }
- },
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvc2NyZWVucy9Tb25nU2VsZWN0U2NyZWVuL09wdGlvbnNQYW5lLmN5LnRzeA==",
- "name": "src/screens/SongSelectScreen/OptionsPane.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/screens/SongSelectScreen/OptionsPane.cy.tsx",
- "baseName": "OptionsPane.cy.tsx",
- "fileName": "OptionsPane",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/screens/SongSelectScreen/OptionsPane.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-10-24 23:57:29 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "0558bd9",
- "subject": "feat: gameplay modifiers (#32)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12YzJOeVpXVnVjeTlUYjI1blUyVnNaV04wVTJOeVpXVnVMMDl3ZEdsdmJuTlFZVzVsTG1ONUxuUnplQ0o5",
- "fetchingStatus": "NOT_FETCHED",
- "data": null,
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvc2NyZWVucy9zdW1tYXJ5L1N1bW1hcnlTY3JlZW4uY3kudHN4",
- "name": "src/screens/summary/SummaryScreen.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/screens/summary/SummaryScreen.cy.tsx",
- "baseName": "SummaryScreen.cy.tsx",
- "fileName": "SummaryScreen",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/screens/summary/SummaryScreen.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-11-09 20:22:48 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "7712c95",
- "subject": "chore: update to Cypress 11",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12YzJOeVpXVnVjeTl6ZFcxdFlYSjVMMU4xYlcxaGNubFRZM0psWlc0dVkza3VkSE40SW4wPQ==",
- "fetchingStatus": "NOT_FETCHED",
- "data": null,
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvc2NyZWVucy9nYW1lcGxheS9HYW1lcGxheUxvYWRpbmcuY3kudHN4",
- "name": "src/screens/gameplay/GameplayLoading.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/screens/gameplay/GameplayLoading.cy.tsx",
- "baseName": "GameplayLoading.cy.tsx",
- "fileName": "GameplayLoading",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/screens/gameplay/GameplayLoading.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-11-01 21:26:36 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "6623c5c",
- "subject": "feat: add loading screen when streaming song (#40)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12YzJOeVpXVnVjeTluWVcxbGNHeGhlUzlIWVcxbGNHeGhlVXh2WVdScGJtY3VZM2t1ZEhONEluMD0=",
- "fetchingStatus": "NOT_FETCHED",
- "data": null,
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvc2NyZWVucy9nYW1lcGxheS9jb21wb25lbnRzL0dhbWVwbGF5L0dhbWVwbGF5LmN5LnRz",
- "name": "src/screens/gameplay/components/Gameplay/Gameplay.cy.ts",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/screens/gameplay/components/Gameplay/Gameplay.cy.ts",
- "baseName": "Gameplay.cy.ts",
- "fileName": "Gameplay",
- "specFileExtension": ".cy.ts",
- "fileExtension": ".ts",
- "relative": "src/screens/gameplay/components/Gameplay/Gameplay.cy.ts",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-11-09 20:22:48 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "7712c95",
- "subject": "chore: update to Cypress 11",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12YzJOeVpXVnVjeTluWVcxbGNHeGhlUzlqYjIxd2IyNWxiblJ6TDBkaGJXVndiR0Y1TDBkaGJXVndiR0Y1TG1ONUxuUnpJbjA9",
- "fetchingStatus": "NOT_FETCHED",
- "data": null,
- "__typename": "RemoteFetchableCloudProjectSpecResult"
- },
- "__typename": "Spec"
- },
- {
- "id": "U3BlYzovVXNlcnMvbGFjaGxhbm1pbGxlci9jb2RlL2R1bXAvZWxldXRoZXJpYS9wYWNrYWdlcy9mcm9udGVuZC9zcmMvc2NyZWVucy9nYW1lcGxheS9jb21wb25lbnRzL0dhbWVwbGF5L0dhbWVwbGF5U2NvcmUuY3kudHN4",
- "name": "src/screens/gameplay/components/Gameplay/GameplayScore.cy.tsx",
- "specType": "component",
- "absolute": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/src/screens/gameplay/components/Gameplay/GameplayScore.cy.tsx",
- "baseName": "GameplayScore.cy.tsx",
- "fileName": "GameplayScore",
- "specFileExtension": ".cy.tsx",
- "fileExtension": ".tsx",
- "relative": "src/screens/gameplay/components/Gameplay/GameplayScore.cy.tsx",
- "gitInfo": {
- "lastModifiedTimestamp": "2022-11-11 11:49:31 +1000",
- "lastModifiedHumanReadable": "3 months ago",
- "author": "Lachlan Miller",
- "statusType": "unmodified",
- "shortHash": "3d3969b",
- "subject": "wip: animation (#42)",
- "__typename": "GitInfo"
- },
- "cloudSpec": {
- "id": "UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo3YWNkNDI4YzFlMmExMGU2ZWU4YmRhMWZjMTQ4OTE5NzdmZTI0ZTk5OmV5Sm1jbTl0UW5KaGJtTm9Jam9pYldGcGJpSXNJbkJ5YjJwbFkzUlRiSFZuSWpvaU4zQTFkV05sSWl3aWMzQmxZMUJoZEdnaU9pSnpjbU12YzJOeVpXVnVjeTluWVcxbGNHeGhlUzlqYjIxd2IyNWxiblJ6TDBkaGJXVndiR0Y1TDBkaGJXVndiR0Y1VTJOdmNtVXVZM2t1ZEhONEluMD0=",
- "fetchingStatus": "NOT_FETCHED",
- "data": null,
- "__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": "experimentalCspAllowList"
- },
- {
- "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": true,
- "from": "default",
- "field": "video"
- },
- {
- "value": 32,
- "from": "default",
- "field": "videoCompression"
- },
- {
- "value": "cypress/videos",
- "from": "default",
- "field": "videosFolder"
- },
- {
- "value": true,
- "from": "default",
- "field": "videoUploadOnPasses"
- },
- {
- "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": 1675067256771,
- "lastProjectId": "7p5uce",
- "specFilter": "",
- "banners": {
- "aci_082022_record": {
- "lastShown": 1675061062102
- }
- }
- },
- "configFile": "cypress.config.ts",
- "configFileAbsolutePath": "/Users/lachlanmiller/code/dump/eleutheria/packages/frontend/cypress.config.ts",
- "projectId": "7p5uce",
- "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"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer_BranchInfo.json b/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer_BranchInfo.json
deleted file mode 100644
index 515d68773c..0000000000
--- a/packages/app/cypress/fixtures/debug-Passing/gql-SpecsPageContainer_BranchInfo.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "data": {
- "currentProject": {
- "id": "Q3VycmVudFByb2plY3Q6L1VzZXJzL2xhY2hsYW5taWxsZXIvY29kZS9kdW1wL2VsZXV0aGVyaWEvcGFja2FnZXMvZnJvbnRlbmQ=",
- "branch": "main",
- "projectId": "7p5uce",
- "__typename": "CurrentProject"
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/src/composables/useCloudSpecData.ts b/packages/app/src/composables/useCloudSpecData.ts
index c0e0ba7122..ed0756c0da 100644
--- a/packages/app/src/composables/useCloudSpecData.ts
+++ b/packages/app/src/composables/useCloudSpecData.ts
@@ -27,7 +27,6 @@ type NonNullCloudSpec = Exclude,
isOffline: Ref,
- projectId: string | null | undefined,
mostRecentUpdate: Ref,
displayedSpecs: Ref<(SpecsListFragment | undefined)[]>,
allSpecs: (SpecsListFragment | undefined)[],
diff --git a/packages/app/src/composables/useRelevantRun.ts b/packages/app/src/composables/useRelevantRun.ts
index 549fbf8acd..9327e86762 100644
--- a/packages/app/src/composables/useRelevantRun.ts
+++ b/packages/app/src/composables/useRelevantRun.ts
@@ -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,
diff --git a/packages/app/src/pages/Runs.vue b/packages/app/src/pages/Runs.vue
index 3595f50c4f..6edb245b75 100644
--- a/packages/app/src/pages/Runs.vue
+++ b/packages/app/src/pages/Runs.vue
@@ -5,6 +5,7 @@
diff --git a/packages/app/src/pages/Specs/Index.vue b/packages/app/src/pages/Specs/Index.vue
index 7bf70f8095..b9eb524adc 100644
--- a/packages/app/src/pages/Specs/Index.vue
+++ b/packages/app/src/pages/Specs/Index.vue
@@ -21,18 +21,27 @@
:is-default-spec-pattern="isDefaultSpecPattern"
@showCreateSpecModal="showCreateSpecModal"
/>
+
diff --git a/packages/app/src/specs/flaky-badge/FlakyInformation.cy.tsx b/packages/app/src/specs/flaky-badge/FlakyInformation.cy.tsx
index a138e70971..531ef9b0ff 100644
--- a/packages/app/src/specs/flaky-badge/FlakyInformation.cy.tsx
+++ b/packages/app/src/specs/flaky-badge/FlakyInformation.cy.tsx
@@ -22,10 +22,14 @@ describe('', () => {
data: {
__typename: 'CloudProjectSpec',
id: '3',
- isConsideredFlaky: flaky,
- flakyStatus: {
+ isConsideredFlakyForRunIds: flaky,
+ flakyStatusForRunIds: {
__typename: 'CloudProjectSpecFlakyStatus',
dashboardUrl: '#',
+ flakyRuns: 1,
+ flakyRunsWindow: 5,
+ lastFlaky: 3,
+ severity: 'LOW',
},
},
}
diff --git a/packages/app/src/specs/flaky-badge/FlakyInformation.vue b/packages/app/src/specs/flaky-badge/FlakyInformation.vue
index 341ef822ff..289cede099 100644
--- a/packages/app/src/specs/flaky-badge/FlakyInformation.vue
+++ b/packages/app/src/specs/flaky-badge/FlakyInformation.vue
@@ -20,12 +20,13 @@
:href="cloudUrl"
class="hocus:no-underline"
>
-
@@ -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',
diff --git a/packages/app/src/specs/flaky-badge/FlakySpecSummaryAdapter.vue b/packages/app/src/specs/flaky-badge/FlakySpecSummaryAdapter.vue
deleted file mode 100644
index a7a0e0f28e..0000000000
--- a/packages/app/src/specs/flaky-badge/FlakySpecSummaryAdapter.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts
index 22d6bbe806..52c965fe45 100644
--- a/packages/data-context/src/DataContext.ts
+++ b/packages/data-context/src/DataContext.ts
@@ -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)
diff --git a/packages/data-context/src/sources/GitDataSource.ts b/packages/data-context/src/sources/GitDataSource.ts
index f3bd23e8e9..3f7ab8caa5 100644
--- a/packages/data-context/src/sources/GitDataSource.ts
+++ b/packages/data-context/src/sources/GitDataSource.ts
@@ -467,6 +467,7 @@ export class GitDataSource {
}
__setGitHashesForTesting (hashes: string[]) {
+ debug('Setting git hashes for testing', hashes)
this.#gitHashes = hashes
}
}
diff --git a/packages/data-context/src/sources/RelevantRunSpecsDataSource.ts b/packages/data-context/src/sources/RelevantRunSpecsDataSource.ts
index 87fe378869..2f618a2efb 100644
--- a/packages/data-context/src/sources/RelevantRunSpecsDataSource.ts
+++ b/packages/data-context/src/sources/RelevantRunSpecsDataSource.ts
@@ -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)) {
diff --git a/packages/data-context/src/sources/RelevantRunsDataSource.ts b/packages/data-context/src/sources/RelevantRunsDataSource.ts
index a0c1cd6e7f..bce910659d 100644
--- a/packages/data-context/src/sources/RelevantRunsDataSource.ts
+++ b/packages/data-context/src/sources/RelevantRunsDataSource.ts
@@ -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,
}
diff --git a/packages/data-context/src/sources/RemotePollingDataSource.ts b/packages/data-context/src/sources/RemotePollingDataSource.ts
deleted file mode 100644
index 598d839ee4..0000000000
--- a/packages/data-context/src/sources/RemotePollingDataSource.ts
+++ /dev/null
@@ -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>({
- 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()
- }
- },
- })
- }
-}
diff --git a/packages/data-context/src/sources/index.ts b/packages/data-context/src/sources/index.ts
index 0ee2b9a4b9..6662e1d209 100644
--- a/packages/data-context/src/sources/index.ts
+++ b/packages/data-context/src/sources/index.ts
@@ -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'
diff --git a/packages/data-context/test/unit/sources/RelevantRunsDataSource.spec.ts b/packages/data-context/test/unit/sources/RelevantRunsDataSource.spec.ts
index 286c4e19cd..78363d088d 100644
--- a/packages/data-context/test/unit/sources/RelevantRunsDataSource.spec.ts
+++ b/packages/data-context/test/unit/sources/RelevantRunsDataSource.spec.ts
@@ -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,
})
diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql
index b9562d0b0f..fede6f0c64 100644
--- a/packages/graphql/schemas/schema.graphql
+++ b/packages/graphql/schemas/schema.graphql
@@ -1704,9 +1704,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!
@@ -2057,12 +2054,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!
@@ -2075,7 +2078,9 @@ type RelevantRunInfo {
enum RelevantRunLocationEnum {
DEBUG
+ RUNS
SIDEBAR
+ SPECS
}
"""
@@ -2347,11 +2352,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 {
diff --git a/packages/graphql/src/plugins/nexusSlowGuardPlugin.ts b/packages/graphql/src/plugins/nexusSlowGuardPlugin.ts
index d87cf7cc87..9c592a961b 100644
--- a/packages/graphql/src/plugins/nexusSlowGuardPlugin.ts
+++ b/packages/graphql/src/plugins/nexusSlowGuardPlugin.ts
@@ -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)
diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts
index cb3c612859..323d96d727 100644
--- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts
+++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts
@@ -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',
diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-RelevantRun.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-RelevantRun.ts
index 64834f5bb5..4c6526a207 100644
--- a/packages/graphql/src/schemaTypes/objectTypes/gql-RelevantRun.ts
+++ b/packages/graphql/src/schemaTypes/objectTypes/gql-RelevantRun.ts
@@ -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',
})
diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Subscription.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Subscription.ts
index 67a4d2ba3d..02eac2e074 100644
--- a/packages/graphql/src/schemaTypes/objectTypes/gql-Subscription.ts
+++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Subscription.ts
@@ -1,5 +1,5 @@
import type { PushFragmentData } from '@packages/data-context/src/actions'
-import { enumType, idArg, list, nonNull, objectType, stringArg, subscriptionType } from 'nexus'
+import { enumType, idArg, list, nonNull, objectType, subscriptionType } from 'nexus'
import { CurrentProject, DevState, Query, Wizard } from '.'
import { Spec } from './gql-Spec'
import { RelevantRun } from './gql-RelevantRun'
@@ -114,25 +114,13 @@ export const Subscription = subscriptionType({
resolve: (source: PushFragmentData[], args, ctx) => source,
})
- t.string('startPollingForSpecs', {
- args: {
- projectId: stringArg(),
- branchName: stringArg(),
- },
- description: 'Initiates the polling mechanism with the Cypress Cloud to check if we should refetch specs, and mark specs as stale if we have updates',
- subscribe: (source, args, ctx) => {
- return ctx.remotePolling.subscribeAndPoll(args.branchName, args.projectId)
- },
- resolve: (o: string | null) => o,
- })
-
t.field('relevantRuns', {
type: RelevantRun,
description: 'Subscription that polls the cloud for new relevant runs that match local git commit hashes',
args: {
location: nonNull(enumType({
name: 'RelevantRunLocationEnum',
- members: ['DEBUG', 'SIDEBAR'],
+ members: ['DEBUG', 'SIDEBAR', 'RUNS', 'SPECS'],
})),
},
subscribe: (source, args, ctx) => {
diff --git a/packages/graphql/test/stubCloudTypes.ts b/packages/graphql/test/stubCloudTypes.ts
index 28cb5205ad..f73bb3c945 100644
--- a/packages/graphql/test/stubCloudTypes.ts
+++ b/packages/graphql/test/stubCloudTypes.ts
@@ -26,6 +26,7 @@ import type {
CloudSpecRun,
CloudTestResult,
CloudRunGroup,
+ CloudProjectRunsByCommitShasArgs,
} from '../src/gen/test-cloud-graphql-types.gen'
import type { GraphQLResolveInfo } from 'graphql'
import type { DebugTestingProgress_SpecsSubscription } from '@packages/app/src/generated/graphql'
@@ -135,6 +136,22 @@ export function createCloudProject (config: Partial>) {
nodes: connectionData.edges.map((e) => e.node),
}
},
+ runsByCommitShas (args: CloudProjectRunsByCommitShasArgs) {
+ return args.commitShas?.map((sha, i) => {
+ const statusIndex = i % STATUS_ARRAY.length
+ const status = STATUS_ARRAY[statusIndex]
+
+ return createCloudRun({
+ status,
+ totalPassed: i,
+ url: `http://dummy.cypress.io/runs/${i}`,
+ commitInfo: createCloudRunCommitInfo({
+ sha,
+ summary: `fix: using Git data ${status}`,
+ }),
+ })
+ })
+ },
...config,
} as CloudProject
@@ -356,7 +373,10 @@ export function createCloudProjectSpecResult (config: Partial)
},
specRunsForRunIds: [],
averageDurationForRunIds: 1234,
- flakyStatusForRunIds: null,
+ flakyStatusForRunIds: {
+ __typename: 'CloudProjectSpecFlakyStatus',
+ severity: 'NONE',
+ },
isConsideredFlakyForRunIds: false,
...config,
}
From 89f0fb64650d1795a912773b5c0660d1aceea0bc Mon Sep 17 00:00:00 2001
From: Chris Breiding
Date: Fri, 16 Jun 2023 10:45:53 -0400
Subject: [PATCH 3/4] chore: internal refactor of privileged commands (#27060)
---
.circleci/workflows.yml | 3 +-
cli/types/cypress-eventemitter.d.ts | 4 +-
packages/app/.gitignore | 3 +-
.../e2e/runner/reporter-ct-generator.ts | 12 --
.../e2e/runner/reporter.command_errors.cy.ts | 12 --
packages/app/src/runner/event-manager.ts | 8 +-
packages/app/src/runner/index.ts | 31 ++-
packages/driver/.gitignore | 1 +
.../driver/cypress/e2e/commands/exec.cy.js | 28 ++-
.../driver/cypress/e2e/commands/files.cy.js | 114 +++++++----
.../driver/cypress/e2e/commands/task.cy.js | 26 ++-
.../cypress/e2e/cypress/script_utils.cy.js | 79 +++++++-
.../e2e/e2e/origin/commands/files.cy.ts | 14 +-
.../e2e/e2e/origin/commands/misc.cy.ts | 2 +-
.../cypress/e2e/e2e/origin/validation.cy.ts | 40 ++--
.../cypress/e2e/e2e/privileged_commands.cy.ts | 191 ++++++++++++++++++
.../driver/cypress/fixtures/aut-commands.html | 2 +
.../driver/cypress/fixtures/aut-commands.js | 97 +++++++++
packages/driver/cypress/plugins/server.js | 19 +-
packages/driver/cypress/support/defaults.js | 23 ++-
packages/driver/cypress/support/utils.ts | 23 +++
.../driver/src/cross-origin/communicator.ts | 10 +
.../driver/src/cross-origin/events/socket.ts | 4 +
packages/driver/src/cross-origin/origin_fn.ts | 9 +-
.../src/cy/commands/actions/selectFile.ts | 96 +++++++--
packages/driver/src/cy/commands/exec.ts | 36 +++-
packages/driver/src/cy/commands/files.ts | 62 +++++-
.../driver/src/cy/commands/origin/index.ts | 42 +++-
packages/driver/src/cy/commands/task.ts | 32 ++-
packages/driver/src/cypress.ts | 15 +-
packages/driver/src/cypress/chainer.ts | 4 +-
packages/driver/src/cypress/cy.ts | 14 +-
packages/driver/src/cypress/error_messages.ts | 1 +
packages/driver/src/cypress/script_utils.ts | 40 +++-
.../driver/src/util/privileged_channel.ts | 40 ++++
packages/driver/types/internal-types.d.ts | 4 +-
packages/driver/types/spec-types.d.ts | 2 +
packages/server/lib/controllers/files.js | 65 +++---
packages/server/lib/files.js | 30 ++-
packages/server/lib/html/iframe.html | 13 +-
.../server/lib/html/spec-bridge-iframe.html | 9 +-
.../privileged-commands/privileged-channel.js | 159 +++++++++++++++
.../privileged-commands-manager.ts | 126 ++++++++++++
packages/server/lib/routes-e2e.ts | 36 +---
packages/server/lib/socket-base.ts | 20 +-
packages/server/lib/util/fs.ts | 1 +
.../test/unit/browsers/cri-client_spec.ts | 24 ++-
.../unit/browsers/privileged-channel_spec.js | 180 +++++++++++++++++
packages/server/test/unit/files_spec.js | 38 ++--
packages/server/test/unit/socket_spec.js | 34 +---
system-tests/__snapshots__/cdp_spec.ts.js | 26 ---
.../cypress/e2e/errors/readfile.cy.js | 5 -
.../cypress/e2e/spec.cy.ts | 10 -
.../remote-debugging-disconnect/plugins.js | 6 -
system-tests/test/cdp_spec.ts | 14 --
yarn.lock | 7 +-
56 files changed, 1561 insertions(+), 385 deletions(-)
create mode 100644 packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts
create mode 100644 packages/driver/cypress/fixtures/aut-commands.html
create mode 100644 packages/driver/cypress/fixtures/aut-commands.js
create mode 100644 packages/driver/src/util/privileged_channel.ts
create mode 100644 packages/server/lib/privileged-commands/privileged-channel.js
create mode 100644 packages/server/lib/privileged-commands/privileged-commands-manager.ts
create mode 100644 packages/server/test/unit/browsers/privileged-channel_spec.js
delete mode 100644 system-tests/__snapshots__/cdp_spec.ts.js
delete mode 100644 system-tests/project-fixtures/runner-specs/cypress/e2e/errors/readfile.cy.js
diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml
index e4856ddd94..6494845bca 100644
--- a/.circleci/workflows.yml
+++ b/.circleci/workflows.yml
@@ -74,8 +74,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- - equal: [ 'matth/feat/chrome-headless', << pipeline.git.branch >> ]
- - equal: [ 'lmiller/fix-windows-regressions', << pipeline.git.branch >> ]
+ - equal: [ 'privileged-commands-refactor', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
diff --git a/cli/types/cypress-eventemitter.d.ts b/cli/types/cypress-eventemitter.d.ts
index 86d2587f72..adf6279c79 100644
--- a/cli/types/cypress-eventemitter.d.ts
+++ b/cli/types/cypress-eventemitter.d.ts
@@ -3,8 +3,8 @@ type EventEmitter2 = import("eventemitter2").EventEmitter2
interface CyEventEmitter extends Omit {
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
diff --git a/packages/app/.gitignore b/packages/app/.gitignore
index 2fc8f018b1..d4a187f56e 100644
--- a/packages/app/.gitignore
+++ b/packages/app/.gitignore
@@ -1,4 +1,5 @@
cypress/videos/*
cypress/screenshots/*
+cypress/downloads/*
-components.d.ts
\ No newline at end of file
+components.d.ts
diff --git a/packages/app/cypress/e2e/runner/reporter-ct-generator.ts b/packages/app/cypress/e2e/runner/reporter-ct-generator.ts
index 8575230604..f5a6e3778c 100644
--- a/packages/app/cypress/e2e/runner/reporter-ct-generator.ts
+++ b/packages/app/cypress/e2e/runner/reporter-ct-generator.ts
@@ -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',
diff --git a/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts b/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts
index a98045bdd5..a17b14057b 100644
--- a/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts
+++ b/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts
@@ -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',
diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts
index e8cb2180f8..9b723b3f8f 100644
--- a/packages/app/src/runner/event-manager.ts
+++ b/packages/app/src/runner/event-manager.ts
@@ -750,7 +750,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)
})
diff --git a/packages/app/src/runner/index.ts b/packages/app/src/runner/index.ts
index 69dd26557d..dfe16a8a04 100644
--- a/packages/app/src/runner/index.ts
+++ b/packages/app/src/runner/index.ts
@@ -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 = 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
/**
diff --git a/packages/driver/.gitignore b/packages/driver/.gitignore
index 944d461994..2c3d561d91 100644
--- a/packages/driver/.gitignore
+++ b/packages/driver/.gitignore
@@ -1,2 +1,3 @@
cypress/videos
cypress/screenshots
+cypress/downloads
diff --git a/packages/driver/cypress/e2e/commands/exec.cy.js b/packages/driver/cypress/e2e/commands/exec.cy.js
index 0d20f78616..9da38e3b1f 100644
--- a/packages/driver/cypress/e2e/commands/exec.cy.js
+++ b/packages/driver/cypress/e2e/commands/exec.cy.js
@@ -12,14 +12,18 @@ describe('src/cy/commands/exec', () => {
cy.stub(Cypress, 'backend').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')
})
diff --git a/packages/driver/cypress/e2e/commands/files.cy.js b/packages/driver/cypress/e2e/commands/files.cy.js
index 8648ee4bae..e38b503c95 100644
--- a/packages/driver/cypress/e2e/commands/files.cy.js
+++ b/packages/driver/cypress/e2e/commands/files.cy.js
@@ -14,14 +14,20 @@ describe('src/cy/commands/files', () => {
})
describe('#readFile', () => {
- it('triggers \'read:file\' with the right options', () => {
+ 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',
+ },
+ },
)
})
})
@@ -31,9 +37,15 @@ describe('src/cy/commands/files', () => {
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',
+ },
+ },
)
})
})
@@ -47,9 +59,15 @@ describe('src/cy/commands/files', () => {
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'))
})
@@ -426,17 +444,21 @@ describe('src/cy/commands/files', () => {
})
describe('#writeFile', () => {
- it('triggers \'write:file\' with the right options', () => {
+ 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',
+ },
},
)
})
@@ -447,12 +469,16 @@ describe('src/cy/commands/files', () => {
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',
+ },
},
)
})
@@ -462,14 +488,20 @@ describe('src/cy/commands/files', () => {
it('explicit null encoding is sent to server as Buffer', () => {
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',
+ },
},
)
})
@@ -480,12 +512,16 @@ describe('src/cy/commands/files', () => {
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',
+ },
},
)
})
@@ -531,12 +567,16 @@ describe('src/cy/commands/files', () => {
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+',
+ },
},
)
})
diff --git a/packages/driver/cypress/e2e/commands/task.cy.js b/packages/driver/cypress/e2e/commands/task.cy.js
index 85d92bdf69..cee3237adb 100644
--- a/packages/driver/cypress/e2e/commands/task.cy.js
+++ b/packages/driver/cypress/e2e/commands/task.cy.js
@@ -9,14 +9,18 @@ describe('src/cy/commands/task', () => {
cy.stub(Cypress, 'backend').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' },
},
})
})
diff --git a/packages/driver/cypress/e2e/cypress/script_utils.cy.js b/packages/driver/cypress/e2e/cypress/script_utils.cy.js
index e473c6cb7c..1fa77e6a7b 100644
--- a/packages/driver/cypress/e2e/cypress/script_utils.cy.js
+++ b/packages/driver/cypress/e2e/cypress/script_utils.cy.js
@@ -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)
})
diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/files.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/files.cy.ts
index d708b9b9fc..5181fe038c 100644
--- a/packages/driver/cypress/e2e/e2e/origin/commands/files.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/commands/files.cy.ts
@@ -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',
+ },
},
)
})
diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
index e3d03b1b69..0e376fbd3d 100644
--- a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
@@ -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 = [
diff --git a/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts b/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts
index d5f957330c..dd525e57f4 100644
--- a/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/validation.cy.ts
@@ -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)
diff --git a/packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts b/packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts
new file mode 100644
index 0000000000..0f8a59082b
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts
@@ -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
diff --git a/packages/driver/cypress/fixtures/aut-commands.js b/packages/driver/cypress/fixtures/aut-commands.js
new file mode 100644
index 0000000000..806329d917
--- /dev/null
+++ b/packages/driver/cypress/fixtures/aut-commands.js
@@ -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}`)
+ }
+})()
diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js
index f42ed60e46..d306562347 100644
--- a/packages/driver/cypress/plugins/server.js
+++ b/packages/driver/cypress/plugins/server.js
@@ -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)
}
-