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) } - + + `) + }) + + app.get('/aut-commands', async (req, res) => { + const script = (await fs.readFileAsync(path.join(__dirname, '..', 'fixtures', 'aut-commands.js'))).toString() + + res.send(` + + + + + + `) }) diff --git a/packages/driver/cypress/support/defaults.js b/packages/driver/cypress/support/defaults.js index 332f3834cc..400103b4ee 100644 --- a/packages/driver/cypress/support/defaults.js +++ b/packages/driver/cypress/support/defaults.js @@ -11,7 +11,9 @@ if (!isActuallyInteractive) { Cypress.config('retries', 2) } -beforeEach(() => { +let ranPrivilegedCommandsInBeforeEach = false + +beforeEach(function () { // always set that we're interactive so we // get consistent passes and failures when running // from CI and when running in GUI mode @@ -30,6 +32,25 @@ beforeEach(() => { try { $(cy.state('window')).off() } catch (error) {} // eslint-disable-line no-empty + + // only want to run this as part of the privileged commands spec + if (cy.config('spec').baseName === 'privileged_commands.cy.ts') { + cy.visit('/fixtures/files-form.html') + + // it only needs to run once per spec run + if (ranPrivilegedCommandsInBeforeEach) return + + ranPrivilegedCommandsInBeforeEach = true + + cy.exec('echo "hello"') + cy.readFile('cypress/fixtures/app.json') + cy.writeFile('cypress/_test-output/written.json', 'contents') + cy.task('return:arg', 'arg') + cy.get('#basic').selectFile('cypress/fixtures/valid.json') + if (!Cypress.isBrowser({ family: 'webkit' })) { + cy.origin('http://foobar.com:3500', () => {}) + } + } }) // this is here to test that cy.origin() dependencies used directly in the diff --git a/packages/driver/cypress/support/utils.ts b/packages/driver/cypress/support/utils.ts index 13050d662e..1e0283cef1 100644 --- a/packages/driver/cypress/support/utils.ts +++ b/packages/driver/cypress/support/utils.ts @@ -172,6 +172,29 @@ export const makeRequestForCookieBehaviorTests = ( }) } +function runCommands () { + cy.exec('echo "hello"') + cy.readFile('cypress/fixtures/app.json') + cy.writeFile('cypress/_test-output/written.json', 'contents') + cy.task('return:arg', 'arg') + cy.get('#basic').selectFile('cypress/fixtures/valid.json') + if (!Cypress.isBrowser({ family: 'webkit' })) { + cy.origin('http://foobar.com:3500', () => {}) + } +} + +export const runImportedPrivilegedCommands = runCommands + +declare global { + interface Window { + runGlobalPrivilegedCommands: () => void + } +} + +window.runGlobalPrivilegedCommands = runCommands + +Cypress.Commands.add('runSupportFileCustomPrivilegedCommands', runCommands) + Cypress.Commands.addQuery('getAll', getAllFn) Cypress.Commands.add('shouldWithTimeout', shouldWithTimeout) diff --git a/packages/driver/src/cross-origin/communicator.ts b/packages/driver/src/cross-origin/communicator.ts index 2bb6af6035..550a6955f7 100644 --- a/packages/driver/src/cross-origin/communicator.ts +++ b/packages/driver/src/cross-origin/communicator.ts @@ -167,6 +167,16 @@ export class PrimaryOriginCommunicator extends EventEmitter { preprocessedData.args = data.args } + // if the data has an error/err, it needs special handling for Firefox or + // else it will end up ignored because it's not structured-cloneable + if (data?.error) { + preprocessedData.error = preprocessForSerialization(data.error) + } + + if (data?.err) { + preprocessedData.err = preprocessForSerialization(data.err) + } + // If there is no crossOriginDriverWindows, there is no need to send the message. source.postMessage({ event, diff --git a/packages/driver/src/cross-origin/events/socket.ts b/packages/driver/src/cross-origin/events/socket.ts index e74058cfb7..a159e1ea55 100644 --- a/packages/driver/src/cross-origin/events/socket.ts +++ b/packages/driver/src/cross-origin/events/socket.ts @@ -8,6 +8,10 @@ export const handleSocketEvents = (Cypress) => { timeout: Cypress.config().defaultCommandTimeout, }) + if (response && response.error) { + return callback({ error: response.error }) + } + callback({ response }) } diff --git a/packages/driver/src/cross-origin/origin_fn.ts b/packages/driver/src/cross-origin/origin_fn.ts index ac466ad42a..35a4e0902e 100644 --- a/packages/driver/src/cross-origin/origin_fn.ts +++ b/packages/driver/src/cross-origin/origin_fn.ts @@ -181,9 +181,16 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { Cypress.specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncGlobals: true }) }) + // the name of this function is used to verify if privileged commands are + // properly called. it shouldn't be removed and if the name is changed, it + // needs to also be changed in server/lib/browsers/privileged-channel.js + function invokeOriginFn (callback) { + return window.eval(`(${callback})`)(args) + } + try { const callback = await getCallbackFn(fn, file) - const value = window.eval(`(${callback})`)(args) + const value = invokeOriginFn(callback) // If we detect a non promise value with commands in queue, throw an error if (value && cy.queue.length > 0 && !value.then) { diff --git a/packages/driver/src/cy/commands/actions/selectFile.ts b/packages/driver/src/cy/commands/actions/selectFile.ts index 72991131bb..3fccfc4948 100644 --- a/packages/driver/src/cy/commands/actions/selectFile.ts +++ b/packages/driver/src/cy/commands/actions/selectFile.ts @@ -6,6 +6,7 @@ import $dom from '../../../dom' import $errUtils from '../../../cypress/error_utils' import $actionability from '../../actionability' import { addEventCoords, dispatch } from './trigger' +import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel' /* dropzone.js relies on an experimental, nonstandard API, webkitGetAsEntry(). * https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry @@ -82,6 +83,15 @@ interface InternalSelectFileOptions extends Cypress.SelectFileOptions { eventTarget: JQuery } +interface FilePathObject { + fileName?: string + index: number + isFilePath: boolean + lastModified?: number + mimeType?: string + path: string +} + const ACTIONS = { select: (element, dataTransfer, coords, state) => { (element as HTMLInputElement).files = dataTransfer.files @@ -153,32 +163,64 @@ export default (Commands, Cypress, cy, state, config) => { } } - // Uses backend read:file rather than cy.readFile because we don't want to retry - // loading a specific file until timeout, but rather retry the selectFile command as a whole - const handlePath = async (file, options) => { - return Cypress.backend('read:file', file.contents, { encoding: null }) - .then(({ contents }) => { - return { - // We default to the filename on the path, but allow them to override - fileName: basename(file.contents), - ...file, - contents: Cypress.Buffer.from(contents), - } + const readFiles = async (filePaths, options, userArgs) => { + if (!filePaths.length) return [] + + // This reads the file with privileged access in the same manner as + // cy.readFile(). We call directly into the backend rather than calling + // cy.readFile() directly because we don't want to retry loading a specific + // file until timeout, but rather retry the selectFile command as a whole + return runPrivilegedCommand({ + commandName: 'selectFile', + cy, + Cypress: (Cypress as unknown) as InternalCypress.Cypress, + options: { + files: filePaths, + }, + userArgs, + }) + .then((results) => { + return results.map((result) => { + return { + // We default to the filename on the path, but allow them to override + fileName: basename(result.path), + ...result, + contents: Cypress.Buffer.from(result.contents), + } + }) }) .catch((err) => { + if (err.isNonSpec) { + $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', { + args: { cmd: 'selectFile' }, + }) + } + if (err.code === 'ENOENT') { $errUtils.throwErrByPath('files.nonexistent', { - args: { cmd: 'selectFile', file: file.contents, filePath: err.filePath }, + args: { cmd: 'selectFile', file: err.originalFilePath, filePath: err.filePath }, }) } $errUtils.throwErrByPath('files.unexpected_error', { onFail: options._log, - args: { cmd: 'selectFile', action: 'read', file, filePath: err.filePath, error: err.message }, + args: { cmd: 'selectFile', action: 'read', file: err.originalFilePath, filePath: err.filePath, error: err.message }, }) }) } + const getFilePathObject = (file, index) => { + return { + encoding: null, + fileName: file.fileName, + index, + isFilePath: true, + lastModified: file.lastModified, + mimeType: file.mimeType, + path: file.contents, + } + } + /* * Turns a user-provided file - a string shorthand, ArrayBuffer, or object * into an object of form { @@ -191,7 +233,7 @@ export default (Commands, Cypress, cy, state, config) => { * we warn them and suggest how to fix it. */ const parseFile = (options) => { - return async (file: any, index: number, filesArray: any[]): Promise => { + return (file: any, index: number, filesArray: any[]): Cypress.FileReferenceObject | FilePathObject => { if (typeof file === 'string' || ArrayBuffer.isView(file)) { file = { contents: file } } @@ -212,10 +254,13 @@ export default (Commands, Cypress, cy, state, config) => { } if (typeof file.contents === 'string') { - file = handleAlias(file, options) ?? await handlePath(file, options) + // if not an alias, an object representing that the file is a path that + // needs to be read from disk. contents are an empty string to they + // it skips the next check + file = handleAlias(file, options) ?? getFilePathObject(file, index) } - if (!_.isString(file.contents) && !ArrayBuffer.isView(file.contents)) { + if (!file.isFilePath && !_.isString(file.contents) && !ArrayBuffer.isView(file.contents)) { file.contents = JSON.stringify(file.contents) } @@ -223,8 +268,24 @@ export default (Commands, Cypress, cy, state, config) => { } } + async function collectFiles (files, options, userArgs) { + const filesCollection = ([] as (Cypress.FileReference | FilePathObject)[]).concat(files).map(parseFile(options)) + // if there are any file paths, read them from the server in one go + const filePaths = filesCollection.filter((file) => (file as FilePathObject).isFilePath) + const filePathResults = await readFiles(filePaths, options, userArgs) + + // stitch them back into the collection + filePathResults.forEach((filePathResult) => { + filesCollection[filePathResult.index] = _.pick(filePathResult, 'contents', 'fileName', 'mimeType', 'lastModified') + }) + + return filesCollection as Cypress.FileReferenceObject[] + } + Commands.addAll({ prevSubject: 'element' }, { async selectFile (subject: JQuery, files: Cypress.FileReference | Cypress.FileReference[], options: Partial): Promise { + const userArgs = trimUserArgs([files, _.isObject(options) ? { ...options } : undefined]) + options = _.defaults({}, options, { action: 'select', log: true, @@ -287,8 +348,7 @@ export default (Commands, Cypress, cy, state, config) => { } // Make sure files is an array even if the user only passed in one - const filesArray = await Promise.all(([] as Cypress.FileReference[]).concat(files).map(parseFile(options))) - + const filesArray = await collectFiles(files, options, userArgs) const subjectChain = cy.subjectChain() // We verify actionability on the subject, rather than the eventTarget, diff --git a/packages/driver/src/cy/commands/exec.ts b/packages/driver/src/cy/commands/exec.ts index a0131c604e..4a56f68452 100644 --- a/packages/driver/src/cy/commands/exec.ts +++ b/packages/driver/src/cy/commands/exec.ts @@ -3,18 +3,24 @@ import Promise from 'bluebird' import $errUtils from '../../cypress/error_utils' import type { Log } from '../../cypress/log' +import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel' interface InternalExecOptions extends Partial { _log?: Log cmd?: string + timeout: number } export default (Commands, Cypress, cy) => { Commands.addAll({ - exec (cmd: string, userOptions: Partial = {}) { + exec (cmd: string, userOptions: Partial) { + const userArgs = trimUserArgs([cmd, userOptions]) + + userOptions = userOptions || {} + const options: InternalExecOptions = _.defaults({}, userOptions, { log: true, - timeout: Cypress.config('execTimeout'), + timeout: Cypress.config('execTimeout') as number, failOnNonZeroExit: true, env: {}, }) @@ -46,7 +52,13 @@ export default (Commands, Cypress, cy) => { // because we're handling timeouts ourselves cy.clearTimeout() - return Cypress.backend('exec', _.pick(options, 'cmd', 'timeout', 'env')) + return runPrivilegedCommand({ + commandName: 'exec', + cy, + Cypress: (Cypress as unknown) as InternalCypress.Cypress, + options: _.pick(options, 'cmd', 'timeout', 'env'), + userArgs, + }) .timeout(options.timeout) .then((result) => { if (options._log) { @@ -75,20 +87,26 @@ export default (Commands, Cypress, cy) => { }) }) .catch(Promise.TimeoutError, { timedOut: true }, () => { - return $errUtils.throwErrByPath('exec.timed_out', { + $errUtils.throwErrByPath('exec.timed_out', { onFail: options._log, args: { cmd, timeout: options.timeout }, }) }) - .catch((error) => { + .catch((err) => { // re-throw if timedOut error from above - if (error.name === 'CypressError') { - throw error + if (err.name === 'CypressError') { + throw err } - return $errUtils.throwErrByPath('exec.failed', { + if (err.isNonSpec) { + $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', { + args: { cmd: 'exec' }, + }) + } + + $errUtils.throwErrByPath('exec.failed', { onFail: options._log, - args: { cmd, error }, + args: { cmd, error: err }, }) }) }, diff --git a/packages/driver/src/cy/commands/files.ts b/packages/driver/src/cy/commands/files.ts index 02512836a2..b542f961fc 100644 --- a/packages/driver/src/cy/commands/files.ts +++ b/packages/driver/src/cy/commands/files.ts @@ -3,24 +3,34 @@ import { basename } from 'path' import $errUtils from '../../cypress/error_utils' import type { Log } from '../../cypress/log' +import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel' interface InternalReadFileOptions extends Partial { _log?: Log encoding: Cypress.Encodings + timeout: number } interface InternalWriteFileOptions extends Partial { _log?: Log + timeout: number } +type ReadFileOptions = Partial +type WriteFileOptions = Partial + export default (Commands, Cypress, cy, state) => { Commands.addAll({ - readFile (file, encoding, userOptions: Partial = {}) { + readFile (file: string, encoding: Cypress.Encodings | ReadFileOptions | undefined, userOptions?: ReadFileOptions) { + const userArgs = trimUserArgs([file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined]) + if (_.isObject(encoding)) { userOptions = encoding encoding = undefined } + userOptions = userOptions || {} + const options: InternalReadFileOptions = _.defaults({}, userOptions, { // https://github.com/cypress-io/cypress/issues/1558 // If no encoding is specified, then Cypress has historically defaulted @@ -29,7 +39,7 @@ export default (Commands, Cypress, cy, state) => { // to restore the default node behavior. encoding: encoding === undefined ? 'utf8' : encoding, log: true, - timeout: Cypress.config('defaultCommandTimeout'), + timeout: Cypress.config('defaultCommandTimeout') as number, }) const consoleProps = {} @@ -56,18 +66,34 @@ export default (Commands, Cypress, cy, state) => { cy.clearTimeout() const verifyAssertions = () => { - return Cypress.backend('read:file', file, _.pick(options, 'encoding')).timeout(options.timeout) + return runPrivilegedCommand({ + commandName: 'readFile', + cy, + Cypress: (Cypress as unknown) as InternalCypress.Cypress, + options: { + file, + encoding: options.encoding, + }, + userArgs, + }) + .timeout(options.timeout) .catch((err) => { if (err.name === 'TimeoutError') { - return $errUtils.throwErrByPath('files.timed_out', { + $errUtils.throwErrByPath('files.timed_out', { onFail: options._log, args: { cmd: 'readFile', file, timeout: options.timeout }, }) } + if (err.isNonSpec) { + $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', { + args: { cmd: 'readFile' }, + }) + } + // Non-ENOENT errors are not retried if (err.code !== 'ENOENT') { - return $errUtils.throwErrByPath('files.unexpected_error', { + $errUtils.throwErrByPath('files.unexpected_error', { onFail: options._log, args: { cmd: 'readFile', action: 'read', file, filePath: err.filePath, error: err.message }, }) @@ -116,12 +142,16 @@ export default (Commands, Cypress, cy, state) => { return verifyAssertions() }, - writeFile (fileName, contents, encoding, userOptions: Partial = {}) { + writeFile (fileName: string, contents: string, encoding: Cypress.Encodings | WriteFileOptions | undefined, userOptions: WriteFileOptions) { + const userArgs = trimUserArgs([fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined]) + if (_.isObject(encoding)) { userOptions = encoding encoding = undefined } + userOptions = userOptions || {} + const options: InternalWriteFileOptions = _.defaults({}, userOptions, { // https://github.com/cypress-io/cypress/issues/1558 // If no encoding is specified, then Cypress has historically defaulted @@ -168,7 +198,19 @@ export default (Commands, Cypress, cy, state) => { // the timeout ourselves cy.clearTimeout() - return Cypress.backend('write:file', fileName, contents, _.pick(options, 'encoding', 'flag')).timeout(options.timeout) + return runPrivilegedCommand({ + commandName: 'writeFile', + cy, + Cypress: (Cypress as unknown) as InternalCypress.Cypress, + options: { + fileName, + contents, + encoding: options.encoding, + flag: options.flag, + }, + userArgs, + }) + .timeout(options.timeout) .then(({ filePath, contents }) => { consoleProps['File Path'] = filePath consoleProps['Contents'] = contents @@ -183,6 +225,12 @@ export default (Commands, Cypress, cy, state) => { }) } + if (err.isNonSpec) { + return $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', { + args: { cmd: 'writeFile' }, + }) + } + return $errUtils.throwErrByPath('files.unexpected_error', { onFail: options._log, args: { cmd: 'writeFile', action: 'write', file: fileName, filePath: err.filePath, error: err.message }, diff --git a/packages/driver/src/cy/commands/origin/index.ts b/packages/driver/src/cy/commands/origin/index.ts index e71132398b..9a22d71de5 100644 --- a/packages/driver/src/cy/commands/origin/index.ts +++ b/packages/driver/src/cy/commands/origin/index.ts @@ -9,6 +9,7 @@ import { $Location } from '../../../cypress/location' import { LogUtils } from '../../../cypress/log' import logGroup from '../../logGroup' import type { StateFunc } from '../../../cypress/state' +import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel' const reHttp = /^https?:\/\// @@ -23,15 +24,32 @@ const normalizeOrigin = (urlOrDomain) => { return $Location.normalize(origin) } +type OptionsOrFn = { args: T } | (() => {}) +type Fn = (args?: T) => {} + +function stringifyFn (fn?: any) { + return _.isFunction(fn) ? fn.toString() : undefined +} + +function getUserArgs (urlOrDomain: string, optionsOrFn: OptionsOrFn, fn?: Fn) { + return trimUserArgs([ + urlOrDomain, + fn && _.isObject(optionsOrFn) ? { ...optionsOrFn } : stringifyFn(optionsOrFn), + fn ? stringifyFn(fn) : undefined, + ]) +} + export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: StateFunc, config: Cypress.InternalConfig) => { const communicator = Cypress.primaryOriginCommunicator Commands.addAll({ - origin (urlOrDomain: string, optionsOrFn: { args: T } | (() => {}), fn?: (args?: T) => {}) { + origin (urlOrDomain: string, optionsOrFn: OptionsOrFn, fn?: Fn) { if (Cypress.isBrowser('webkit')) { return $errUtils.throwErrByPath('webkit.origin') } + const userArgs = getUserArgs(urlOrDomain, optionsOrFn, fn) + const userInvocationStack = state('current').get('userInvocationStack') // store the invocation stack in the case that `cy.origin` errors @@ -185,9 +203,21 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State const fn = _.isFunction(callbackFn) ? callbackFn.toString() : callbackFn const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, config('projectRoot'))?.absoluteFile - // once the secondary origin page loads, send along the - // user-specified callback to run in that origin try { + // origin is a privileged command, meaning it has to be invoked + // from the spec or support file + await runPrivilegedCommand({ + commandName: 'origin', + cy, + Cypress: (Cypress as unknown) as InternalCypress.Cypress, + options: { + specBridgeOrigin, + }, + userArgs, + }) + + // once the secondary origin page loads, send along the + // user-specified callback to run in that origin communicator.toSpecBridge(origin, 'run:origin:fn', { args: options?.args || undefined, fn, @@ -212,6 +242,12 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State logCounter: LogUtils.getCounter(), }) } catch (err: any) { + if (err.isNonSpec) { + return _reject($errUtils.errByPath('miscellaneous.non_spec_invocation', { + cmd: 'origin', + })) + } + const wrappedErr = $errUtils.errByPath('origin.run_origin_fn_errored', { error: err.message, }) diff --git a/packages/driver/src/cy/commands/task.ts b/packages/driver/src/cy/commands/task.ts index 7223b95084..a3c68af1aa 100644 --- a/packages/driver/src/cy/commands/task.ts +++ b/packages/driver/src/cy/commands/task.ts @@ -5,17 +5,23 @@ import $utils from '../../cypress/utils' import $errUtils from '../../cypress/error_utils' import $stackUtils from '../../cypress/stack_utils' import type { Log } from '../../cypress/log' +import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel' interface InternalTaskOptions extends Partial { _log?: Log + timeout: number } export default (Commands, Cypress, cy) => { Commands.addAll({ - task (task, arg, userOptions: Partial = {}) { + task (task, arg, userOptions: Partial) { + const userArgs = trimUserArgs([task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined]) + + userOptions = userOptions || {} + const options: InternalTaskOptions = _.defaults({}, userOptions, { log: true, - timeout: Cypress.config('taskTimeout'), + timeout: Cypress.config('taskTimeout') as number, }) let consoleOutput @@ -52,10 +58,16 @@ export default (Commands, Cypress, cy) => { // because we're handling timeouts ourselves cy.clearTimeout() - return Cypress.backend('task', { - task, - arg, - timeout: options.timeout, + return runPrivilegedCommand({ + commandName: 'task', + cy, + Cypress: (Cypress as unknown) as InternalCypress.Cypress, + userArgs, + options: { + task, + arg, + timeout: options.timeout, + }, }) .timeout(options.timeout) .then((result) => { @@ -71,7 +83,7 @@ export default (Commands, Cypress, cy) => { args: { task, timeout: options.timeout }, }) }) - .catch({ timedOut: true }, (error) => { + .catch({ timedOut: true }, (error: any) => { $errUtils.throwErrByPath('task.server_timed_out', { onFail: options._log, args: { task, timeout: options.timeout, error: error.message }, @@ -83,6 +95,12 @@ export default (Commands, Cypress, cy) => { throw err } + if (err.isNonSpec) { + $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', { + args: { cmd: 'task' }, + }) + } + err.stack = $stackUtils.normalizedStack(err) if (err?.isKnownError) { diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 1afd7f2b94..6ca4eade67 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -45,6 +45,7 @@ import { setupAutEventHandlers } from './cypress/aut_event_handlers' import type { CachedTestState } from '@packages/types' import * as cors from '@packages/network/lib/cors' +import { setSpecContentSecurityPolicy } from './util/privileged_channel' import { telemetry } from '@packages/telemetry/src/browser' @@ -56,6 +57,8 @@ declare global { Cypress: Cypress.Cypress Runner: any cy: Cypress.cy + // eval doesn't exist on the built-in Window type for some reason + eval (expression: string): any } } @@ -344,7 +347,17 @@ class $Cypress { this.events.proxyTo(this.cy) - $scriptUtils.runScripts(specWindow, scripts) + $scriptUtils.runScripts({ + browser: this.config('browser'), + scripts, + specWindow, + testingType: this.testingType, + }) + .then(() => { + if (this.testingType === 'e2e') { + return setSpecContentSecurityPolicy(specWindow) + } + }) .catch((error) => { this.runner.onSpecError('error')({ error }) }) diff --git a/packages/driver/src/cypress/chainer.ts b/packages/driver/src/cypress/chainer.ts index 939489a7d4..a54a1a1de6 100644 --- a/packages/driver/src/cypress/chainer.ts +++ b/packages/driver/src/cypress/chainer.ts @@ -20,13 +20,15 @@ export class $Chainer { static add (key, fn) { $Chainer.prototype[key] = function (...args) { + const verificationPromise = Cypress.emitMap('command:invocation', { name: key, args }) + const userInvocationStack = $stackUtils.normalizedUserInvocationStack( (new this.specWindow.Error('command invocation stack')).stack, ) // call back the original function with our new args // pass args an as array and not a destructured invocation - fn(this, userInvocationStack, args) + fn(this, userInvocationStack, args, verificationPromise) // return the chainer so additional calls // are slurped up by the chainer instead of cy diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 4c9b08b409..a01a7d9096 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -683,7 +683,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert const cyFn = wrap(true) const chainerFn = wrap(false) - const callback = (chainer, userInvocationStack, args, firstCall = false) => { + const callback = (chainer, userInvocationStack, args, verificationPromise, firstCall = false) => { // dont enqueue / inject any new commands if // onInjectCommand returns false const onInjectCommand = cy.state('onInjectCommand') @@ -699,6 +699,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert chainerId: chainer.chainerId, userInvocationStack, fn: firstCall ? cyFn : chainerFn, + verificationPromise, })) } @@ -707,6 +708,15 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert cy[name] = function (...args) { ensureRunnable(cy, name) + // for privileged commands, we send a message to the server that verifies + // them as coming from the spec. the fulfillment of this promise means + // the message was received. the implementation for those commands + // checks to make sure this promise is fulfilled before sending its + // websocket message for running the command to ensure prevent a race + // condition where running the command happens before the command is + // verified + const verificationPromise = Cypress.emitMap('command:invocation', { name, args }) + // this is the first call on cypress // so create a new chainer instance const chainer = new $Chainer(cy.specWindow) @@ -717,7 +727,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error) - callback(chainer, userInvocationStack, args, true) + callback(chainer, userInvocationStack, args, verificationPromise, true) // if we are in the middle of a command // and its return value is a promise diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index d2b85692e8..a2b055b8e6 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -747,6 +747,7 @@ export default { }, miscellaneous: { + non_spec_invocation: `${cmd('{{cmd}}')} must only be invoked from the spec file or support file.`, returned_value_and_commands_from_custom_command (obj) { return { message: stripIndent`\ diff --git a/packages/driver/src/cypress/script_utils.ts b/packages/driver/src/cypress/script_utils.ts index 801bcee455..e624b0fa0f 100644 --- a/packages/driver/src/cypress/script_utils.ts +++ b/packages/driver/src/cypress/script_utils.ts @@ -42,9 +42,38 @@ const runScriptsFromUrls = (specWindow, scripts) => { .then((scripts) => evalScripts(specWindow, scripts)) } +const appendScripts = (specWindow, scripts) => { + return Bluebird.each(scripts, (script: any) => { + const firstScript = specWindow.document.querySelector('script') + const specScript = specWindow.document.createElement('script') + + return new Promise((resolve) => { + specScript.addEventListener('load', () => { + resolve() + }) + + specScript.src = script.relativeUrl + firstScript.after(specScript) + }) + }) +} + +interface Script { + absolute: string + relative: string + relativeUrl: string +} + +interface RunScriptsOptions { + browser: Cypress.Browser + scripts: Script[] + specWindow: Window + testingType: Cypress.TestingType +} + // Supports either scripts as objects or as async import functions export default { - runScripts: (specWindow, scripts) => { + runScripts: ({ browser, scripts, specWindow, testingType }: RunScriptsOptions) => { // if scripts contains at least one promise if (scripts.length && typeof scripts[0] === 'function') { // chain the loading promises @@ -54,6 +83,15 @@ export default { return Bluebird.each(scripts, (script: any) => script()) } + // in webkit, stack traces for e2e are made pretty much useless if these + // scripts are eval'd, so we append them as script tags instead + if (browser.family === 'webkit' && testingType === 'e2e') { + return appendScripts(specWindow, scripts) + } + + // for other browsers, we get the contents of the scripts so that we can + // extract and utilize the source maps for better errors and code frames. + // we then eval the script contents to run them return runScriptsFromUrls(specWindow, scripts) }, } diff --git a/packages/driver/src/util/privileged_channel.ts b/packages/driver/src/util/privileged_channel.ts new file mode 100644 index 0000000000..3a3e9367bd --- /dev/null +++ b/packages/driver/src/util/privileged_channel.ts @@ -0,0 +1,40 @@ +import _ from 'lodash' +import Bluebird from 'bluebird' + +/** + * prevents further scripts outside of our own and the spec itself from being + * run in the spec frame + * @param specWindow: Window + */ +export function setSpecContentSecurityPolicy (specWindow) { + const metaEl = specWindow.document.createElement('meta') + + metaEl.setAttribute('http-equiv', 'Content-Security-Policy') + metaEl.setAttribute('content', `script-src 'unsafe-eval'`) + specWindow.document.querySelector('head')!.appendChild(metaEl) +} + +interface RunPrivilegedCommandOptions { + commandName: string + cy: Cypress.cy + Cypress: InternalCypress.Cypress + options: any + userArgs: any[] +} + +export function runPrivilegedCommand ({ commandName, cy, Cypress, options, userArgs }: RunPrivilegedCommandOptions): Bluebird { + return Bluebird.try(() => { + return cy.state('current').get('verificationPromise')[0] + }) + .then(() => { + return Cypress.backend('run:privileged', { + commandName, + options, + userArgs, + }) + }) +} + +export function trimUserArgs (args: any[]) { + return _.dropRightWhile(args, _.isUndefined) +} diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index 10e56fb970..56745656b9 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -68,7 +68,9 @@ declare namespace Cypress { } declare namespace InternalCypress { - interface Cypress extends Cypress.Cypress, NodeEventEmitter {} + interface Cypress extends Cypress.Cypress, NodeEventEmitter { + backend: (eventName: string, ...args: any[]) => Promise + } interface LocalStorage extends Cypress.LocalStorage { setStorages: (local, remote) => LocalStorage diff --git a/packages/driver/types/spec-types.d.ts b/packages/driver/types/spec-types.d.ts index 5921972515..048c91280a 100644 --- a/packages/driver/types/spec-types.d.ts +++ b/packages/driver/types/spec-types.d.ts @@ -5,5 +5,7 @@ declare namespace Cypress { originLoadUtils(origin: string): Chainable getAll(...aliases: string[]): Chainable shouldWithTimeout(cb: (subj: {}) => void, timeout?: number): Chainable + runSpecFileCustomPrivilegedCommands(): Chainable + runSupportFileCustomPrivilegedCommands(): Chainable } } diff --git a/packages/server/lib/controllers/files.js b/packages/server/lib/controllers/files.js index 81d34c19ee..97ac1d2f67 100644 --- a/packages/server/lib/controllers/files.js +++ b/packages/server/lib/controllers/files.js @@ -5,46 +5,54 @@ const debug = require('debug')('cypress:server:controllers') const { escapeFilenameInUrl } = require('../util/escape_filename') const { getCtx } = require('@packages/data-context') const { cors } = require('@packages/network') +const { privilegedCommandsManager } = require('../privileged-commands/privileged-commands-manager') module.exports = { - handleIframe (req, res, config, remoteStates, extraOptions) { + async handleIframe (req, res, config, remoteStates, extraOptions) { const test = req.params[0] const iframePath = cwd('lib', 'html', 'iframe.html') const specFilter = _.get(extraOptions, 'specFilter') debug('handle iframe %o', { test, specFilter }) - return this.getSpecs(test, config, extraOptions) - .then((specs) => { - const supportFileJs = this.getSupportFile(config) - const allFilesToSend = specs + const specs = await this.getSpecs(test, config, extraOptions) + const supportFileJs = this.getSupportFile(config) + const allFilesToSend = specs - if (supportFileJs) { - allFilesToSend.unshift(supportFileJs) - } + if (supportFileJs) { + allFilesToSend.unshift(supportFileJs) + } - debug('all files to send %o', _.map(allFilesToSend, 'relative')) + debug('all files to send %o', _.map(allFilesToSend, 'relative')) - const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, { - skipDomainInjectionForDomains: config.experimentalSkipDomainInjection, - }) ? - remoteStates.getPrimary().domainName : - undefined + const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, { + skipDomainInjectionForDomains: config.experimentalSkipDomainInjection, + }) ? + remoteStates.getPrimary().domainName : + undefined - const iframeOptions = { - superDomain, - title: this.getTitle(test), - scripts: JSON.stringify(allFilesToSend), - } - - debug('iframe %s options %o', test, iframeOptions) - - return res.render(iframePath, iframeOptions) + const privilegedChannel = await privilegedCommandsManager.getPrivilegedChannel({ + browserFamily: req.query.browserFamily, + isSpecBridge: false, + namespace: config.namespace, + scripts: allFilesToSend, + url: req.proxiedUrl, }) + + const iframeOptions = { + superDomain, + title: this.getTitle(test), + scripts: JSON.stringify(allFilesToSend), + privilegedChannel, + } + + debug('iframe %s options %o', test, iframeOptions) + + res.render(iframePath, iframeOptions) }, - handleCrossOriginIframe (req, res, config) { + async handleCrossOriginIframe (req, res, config) { const iframePath = cwd('lib', 'html', 'spec-bridge-iframe.html') const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, { skipDomainInjectionForDomains: config.experimentalSkipDomainInjection, @@ -54,10 +62,19 @@ module.exports = { const origin = cors.getOrigin(req.proxiedUrl) + const privilegedChannel = await privilegedCommandsManager.getPrivilegedChannel({ + browserFamily: req.query.browserFamily, + isSpecBridge: true, + namespace: config.namespace, + scripts: [], + url: req.proxiedUrl, + }) + const iframeOptions = { superDomain, title: `Cypress for ${origin}`, namespace: config.namespace, + privilegedChannel, } debug('cross origin iframe with options %o', iframeOptions) diff --git a/packages/server/lib/files.js b/packages/server/lib/files.js index 3add13e81e..ea497c1c40 100644 --- a/packages/server/lib/files.js +++ b/packages/server/lib/files.js @@ -1,9 +1,10 @@ +const Bluebird = require('bluebird') const path = require('path') const { fs } = require('./util/fs') module.exports = { - readFile (projectRoot, file, options = {}) { - const filePath = path.resolve(projectRoot, file) + readFile (projectRoot, options = {}) { + const filePath = path.resolve(projectRoot, options.file) const readFn = (path.extname(filePath) === '.json' && options.encoding !== null) ? fs.readJsonAsync : fs.readFileAsync // https://github.com/cypress-io/cypress/issues/1558 @@ -19,22 +20,39 @@ module.exports = { } }) .catch((err) => { + err.originalFilePath = options.file err.filePath = filePath throw err }) }, - writeFile (projectRoot, file, contents, options = {}) { - const filePath = path.resolve(projectRoot, file) + readFiles (projectRoot, options = {}) { + return Bluebird.map(options.files, (file) => { + return this.readFile(projectRoot, { + file: file.path, + encoding: file.encoding, + }) + .then(({ contents, filePath }) => { + return { + ...file, + filePath, + contents, + } + }) + }) + }, + + writeFile (projectRoot, options = {}) { + const filePath = path.resolve(projectRoot, options.fileName) const writeOptions = { encoding: options.encoding === undefined ? 'utf8' : options.encoding, flag: options.flag || 'w', } - return fs.outputFile(filePath, contents, writeOptions) + return fs.outputFile(filePath, options.contents, writeOptions) .then(() => { return { - contents, + contents: options.contents, filePath, } }) diff --git a/packages/server/lib/html/iframe.html b/packages/server/lib/html/iframe.html index 8ea4099bbf..5225a02f19 100644 --- a/packages/server/lib/html/iframe.html +++ b/packages/server/lib/html/iframe.html @@ -5,18 +5,21 @@ {{title}} - + + diff --git a/packages/server/lib/html/spec-bridge-iframe.html b/packages/server/lib/html/spec-bridge-iframe.html index 7eda53a497..80f92b3ee6 100644 --- a/packages/server/lib/html/spec-bridge-iframe.html +++ b/packages/server/lib/html/spec-bridge-iframe.html @@ -5,11 +5,12 @@ {{title}} - + diff --git a/packages/server/lib/privileged-commands/privileged-channel.js b/packages/server/lib/privileged-commands/privileged-channel.js new file mode 100644 index 0000000000..f9534c4f0b --- /dev/null +++ b/packages/server/lib/privileged-commands/privileged-channel.js @@ -0,0 +1,159 @@ +/* global window */ +(({ browserFamily, isSpecBridge, key, namespace, scripts, url, win = window }) => { + /** + * This file is read as a string in the server and injected into the spec + * frame in order to create a privileged channel between the server and + * the spec frame. The values above are provided by the server, with the + * `key` being particularly important since it is used to validate + * any messages sent from this channel back to the server. + * + * This file does not get preprocessed, so it should not contain syntax that + * our minimum supported browsers do not support. + */ + + const Err = win.Error + const captureStackTrace = win.Error.captureStackTrace + const filter = win.Array.prototype.filter + const arrayIncludes = win.Array.prototype.includes + const map = win.Array.prototype.map + const stringIncludes = win.String.prototype.includes + const replace = win.String.prototype.replace + const split = win.String.prototype.split + const functionToString = win.Function.prototype.toString + const fetch = win.fetch + const parse = win.JSON.parse + const stringify = win.JSON.stringify + + const queryStringRegex = /\?.*$/ + + // since this function is eval'd, the scripts are included as stringified JSON + if (scripts) { + scripts = parse(scripts) + } + + // when privileged commands are called within the cy.origin() callback, + // since the callback is eval'd in the spec bridge instead of being run + // directly in the spec frame, we need to use different criteria, namely + // that the stack includes the function where we eval the callback + const hasSpecBridgeInvocation = (err) => { + switch (browserFamily) { + case 'chromium': + return stringIncludes.call(err.stack, 'at invokeOriginFn') + case 'firefox': + return stringIncludes.call(err.stack, 'invokeOriginFn@') + // currently, this won't run in webkit since it doesn't + // support cy.origin() + default: + return false + } + } + + // in chromium, stacks only include lines from the frame where the error is + // created, so to validate a function call was from the spec frame, we strip + // message lines and any eval calls (since they could be invoked from outside + // the spec frame) and if there are lines left, they must have been from + // the spec frame itself + const hasSpecFrameStackLines = (err) => { + const stackLines = split.call(err.stack, '\n') + const filteredLines = filter.call(stackLines, (line) => { + return ( + !stringIncludes.call(line, err.message) + && !stringIncludes.call(line, 'eval at ') + ) + }) + + return filteredLines.length > 0 + } + + // in non-chromium browsers, the stack will include either the spec file url + // or the support file + const hasStackLinesFromSpecOrSupportFile = (err) => { + return filter.call(scripts, (script) => { + // in webkit, stack line might not include the query string + if (browserFamily === 'webkit') { + script = replace.call(script, queryStringRegex, '') + } + + return stringIncludes.call(err.stack, script) + }).length > 0 + } + + // privileged commands are commands that should only be called from the spec + // because they escape the browser sandbox and (generally) have access to node + const privilegedCommands = [ + 'exec', + // cy.origin() doesn't directly access node, but is a pathway for other + // commands to do so + 'origin', + 'readFile', + // cy.selectFile() accesses node when using the path argument to read a file + 'selectFile', + 'writeFile', + 'task', + ] + + function stackIsFromSpecFrame (err) { + if (isSpecBridge) { + return hasSpecBridgeInvocation(err) + } + + if (browserFamily === 'chromium') { + return hasSpecFrameStackLines(err) + } + + return hasStackLinesFromSpecOrSupportFile(err) + } + + async function onCommandInvocation (command) { + if (!arrayIncludes.call(privilegedCommands, command.name)) return + + // message doesn't really matter since we're only interested in the stack + const err = new Err('command stack error') + + // strips the stack for this function itself, so we get a more accurate + // look at where the command was called from + if (captureStackTrace) { + captureStackTrace.call(Err, err, onCommandInvocation) + } + + // if stack is not validated as being from the spec frame, don't add + // it as a verified command + if (!stackIsFromSpecFrame(err)) return + + const args = map.call([...command.args], (arg) => { + if (typeof arg === 'function') { + return functionToString.call(arg) + } + + return arg + }) + + // if we verify a privileged command was invoked from the spec frame, we + // send it to the server, where it's stored in state. when the command is + // run and it sends its message to the server via websocket, we check + // that verified status before allowing the command to continue running + await fetch(`/${namespace}/add-verified-command`, { + body: stringify({ + args, + name: command.name, + key, + url, + }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }).catch(() => { + // this erroring is unlikely, but it's fine to ignore. if adding the + // verified command failed, the default behavior is NOT to allow + // the privileged command to run + }) + } + + win.Cypress.on('command:invocation', onCommandInvocation) + + // returned for testing purposes only + return { + onCommandInvocation, + } +}) diff --git a/packages/server/lib/privileged-commands/privileged-commands-manager.ts b/packages/server/lib/privileged-commands/privileged-commands-manager.ts new file mode 100644 index 0000000000..90c8130de1 --- /dev/null +++ b/packages/server/lib/privileged-commands/privileged-commands-manager.ts @@ -0,0 +1,126 @@ +import _ from 'lodash' +import os from 'os' +import path from 'path' +import { v4 as uuidv4 } from 'uuid' + +import exec from '../exec' +import files from '../files' +import { fs } from '../util/fs' +import task from '../task' + +export interface SpecChannelOptions { + isSpecBridge: boolean + url: string + key: string +} + +interface SpecOriginatedCommand { + name: string + args: any[] +} + +type NonSpecError = Error & { isNonSpec: boolean | undefined } +type ChannelUrl = string +type ChannelKey = string + +class PrivilegedCommandsManager { + channelKeys: Record = {} + verifiedCommands: SpecOriginatedCommand[] = [] + + async getPrivilegedChannel (options) { + // setting up a non-spec bridge channel means the beginning of running + // a spec and is a signal that we should reset state + if (!options.isSpecBridge) { + this.reset() + } + + // no-op if already set up for url + if (this.channelKeys[options.url]) return + + const key = uuidv4() + + this.channelKeys[options.url] = key + + const script = (await fs.readFileAsync(path.join(__dirname, 'privileged-channel.js'))).toString() + const specScripts = JSON.stringify(options.scripts.map(({ relativeUrl }) => { + if (os.platform() === 'win32') { + return relativeUrl.replaceAll('\\', '\\\\') + } + + return relativeUrl + })) + + return `${script}({ + browserFamily: '${options.browserFamily}', + isSpecBridge: ${options.isSpecBridge || 'false'}, + key: '${key}', + namespace: '${options.namespace}', + scripts: '${specScripts}', + url: '${options.url}' + })` + } + + addVerifiedCommand ({ args, name, key, url }) { + // if the key isn't valid, don't add it as a verified command. once the + // command attempts to run, it will fail at that point + if (key !== this.channelKeys[url]) return + + this.verifiedCommands.push({ name, args }) + } + + // finds and returns matching command from the verified commands array. it + // also removes that command from the verified commands array + hasVerifiedCommand (command) { + const matchingCommand = _.find(this.verifiedCommands, ({ name, args }) => { + return command.name === name && _.isEqual(command.args, _.dropRightWhile(args, _.isUndefined)) + }) + + return !!matchingCommand + } + + runPrivilegedCommand (config, { commandName, options, userArgs }) { + // the presence of the command within the verifiedCommands array indicates + // the command being run is verified + const hasCommand = this.hasVerifiedCommand({ name: commandName, args: userArgs }) + + if (config.testingType === 'e2e' && !hasCommand) { + // this error message doesn't really matter as each command will catch it + // in the driver based on err.isNonSpec and throw a different error + const err = new Error(`cy.${commandName}() must be invoked from the spec file or support file`) as NonSpecError + + err.isNonSpec = true + + throw err + } + + switch (commandName) { + case 'exec': + return exec.run(config.projectRoot, options) + case 'origin': + // only need to verify that it's spec-originated above + return + case 'readFile': + return files.readFile(config.projectRoot, options) + case 'selectFile': + return files.readFiles(config.projectRoot, options) + case 'writeFile': + return files.writeFile(config.projectRoot, options) + case 'task': { + const configFile = config.configFile && config.configFile.includes(config.projectRoot) + ? config.configFile + : path.join(config.projectRoot, config.configFile) + + return task.run(configFile ?? null, options) + } + default: + throw new Error(`You requested a secure backend event for a command we cannot handle: ${commandName}`) + } + } + + reset () { + this.channelKeys = {} + this.verifiedCommands = [] + } +} + +export const privilegedCommandsManager = new PrivilegedCommandsManager() diff --git a/packages/server/lib/routes-e2e.ts b/packages/server/lib/routes-e2e.ts index 426c62ee56..78d0b51777 100644 --- a/packages/server/lib/routes-e2e.ts +++ b/packages/server/lib/routes-e2e.ts @@ -1,7 +1,6 @@ import bodyParser from 'body-parser' import Debug from 'debug' import { Router } from 'express' -import fs from 'fs-extra' import path from 'path' import AppData from './util/app_data' @@ -12,6 +11,7 @@ import client from './controllers/client' import files from './controllers/files' import type { InitializeRoutes } from './routes' import * as plugins from './plugins' +import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager' const debug = Debug('cypress:server:routes-e2e') @@ -19,7 +19,6 @@ export const createRoutesE2E = ({ config, networkProxy, onError, - getSpec, }: InitializeRoutes) => { const routesE2E = Router() @@ -32,26 +31,6 @@ export const createRoutesE2E = ({ specController.handle(test, req, res, config, next, onError) }) - routesE2E.get(`/${config.namespace}/get-file/:filePath`, async (req, res) => { - const { filePath } = req.params - - debug('get file: %s', filePath) - - try { - const contents = await fs.readFile(filePath) - - res.json({ contents: contents.toString() }) - } catch (err) { - const errorMessage = `Getting the file at the following path errored:\nPath: ${filePath}\nError: ${err.stack}` - - debug(errorMessage) - - res.json({ - error: errorMessage, - }) - } - }) - routesE2E.post(`/${config.namespace}/process-origin-callback`, bodyParser.json(), async (req, res) => { try { const { file, fn, projectRoot } = req.body @@ -96,13 +75,6 @@ export const createRoutesE2E = ({ networkProxy.handleSourceMapRequest(req, res) }) - // special fallback - serve local files from the project's root folder - routesE2E.get('/__root/*', (req, res) => { - const file = path.join(config.projectRoot, req.params[0]) - - res.sendFile(file, { etag: false }) - }) - // special fallback - serve dist'd (bundled/static) files from the project path folder routesE2E.get(`/${config.namespace}/bundled/*`, (req, res) => { const file = AppData.getBundledFilePath(config.projectRoot, path.join('src', req.params[0])) @@ -131,5 +103,11 @@ export const createRoutesE2E = ({ files.handleCrossOriginIframe(req, res, config) }) + routesE2E.post(`/${config.namespace}/add-verified-command`, bodyParser.json(), (req, res) => { + privilegedCommandsManager.addVerifiedCommand(req.body) + + res.sendStatus(204) + }) + return routesE2E } diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 5585bd69dc..c1b4affbae 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -2,7 +2,6 @@ import Bluebird from 'bluebird' import Debug from 'debug' import EventEmitter from 'events' import _ from 'lodash' -import path from 'path' import { getCtx } from '@packages/data-context' import { handleGraphQLSocketRequest } from '@packages/graphql/src/makeGraphQLServer' import { onNetStubbingEvent } from '@packages/net-stubbing' @@ -10,10 +9,7 @@ import * as socketIo from '@packages/socket' import firefoxUtil from './browsers/firefox-util' import * as errors from './errors' -import exec from './exec' -import files from './files' import fixture from './fixture' -import task from './task' import { ensureProp } from './util/class-helpers' import { getUserEditor, setUserEditor } from './util/editors' import { openFile, OpenFileDetails } from './util/file-opener' @@ -31,6 +27,7 @@ import type { Socket } from '@packages/socket' import type { RunState, CachedTestState } from '@packages/types' import { cors } from '@packages/network' import memory from './browsers/memory' +import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager' type StartListeningCallbacks = { onSocketConnection: (socket: any) => void @@ -386,11 +383,6 @@ export class SocketBase { debug('backend:request %o', { eventName, args }) const backendRequest = () => { - // TODO: standardize `configFile`; should it be absolute or relative to projectRoot? - const cfgFile = config.configFile && config.configFile.includes(config.projectRoot) - ? config.configFile - : path.join(config.projectRoot, config.configFile) - switch (eventName) { case 'preserve:run:state': runState = args[0] @@ -411,10 +403,6 @@ export class SocketBase { return firefoxUtil.collectGarbage() case 'get:fixture': return getFixture(args[0], args[1]) - case 'read:file': - return files.readFile(config.projectRoot, args[0], args[1]) - case 'write:file': - return files.writeFile(config.projectRoot, args[0], args[1], args[2]) case 'net': return onNetStubbingEvent({ eventName: args[0], @@ -424,10 +412,6 @@ export class SocketBase { getFixture, args, }) - case 'exec': - return exec.run(config.projectRoot, args[0]) - case 'task': - return task.run(cfgFile ?? null, args[0]) case 'save:session': return session.saveSession(args[0]) case 'clear:sessions': @@ -456,6 +440,8 @@ export class SocketBase { return memory.endProfiling() case 'check:memory:pressure': return memory.checkMemoryPressure({ ...args[0], automation }) + case 'run:privileged': + return privilegedCommandsManager.runPrivilegedCommand(config, args[0]) case 'telemetry': return (telemetry.exporter() as OTLPTraceExporterCloud)?.send(args[0], () => {}, (err) => { debug('error exporting telemetry data from browser %s', err) diff --git a/packages/server/lib/util/fs.ts b/packages/server/lib/util/fs.ts index edee60b5eb..5846acecc8 100644 --- a/packages/server/lib/util/fs.ts +++ b/packages/server/lib/util/fs.ts @@ -9,6 +9,7 @@ type Promisified any> interface PromisifiedFsExtra { statAsync: (path: string | Buffer) => Bluebird> removeAsync: Promisified + readFileAsync: Promisified writeFileAsync: Promisified pathExistsAsync: Promisified } diff --git a/packages/server/test/unit/browsers/cri-client_spec.ts b/packages/server/test/unit/browsers/cri-client_spec.ts index 1007ea2c65..896d7fbe68 100644 --- a/packages/server/test/unit/browsers/cri-client_spec.ts +++ b/packages/server/test/unit/browsers/cri-client_spec.ts @@ -46,10 +46,7 @@ describe('lib/browsers/cri-client', function () { }) getClient = () => { - return criClient.create({ - target: DEBUGGER_URL, - onError, - }) + return criClient.create(DEBUGGER_URL, onError) } }) @@ -109,9 +106,12 @@ describe('lib/browsers/cri-client', function () { const client = await getClient() client.send('Page.enable') + // @ts-ignore client.send('Page.foo') + // @ts-ignore client.send('Page.bar') client.send('Network.enable') + // @ts-ignore client.send('Network.baz') // clear out previous calls before reconnect @@ -124,5 +124,21 @@ describe('lib/browsers/cri-client', function () { expect(criStub.send).to.be.calledWith('Page.enable') expect(criStub.send).to.be.calledWith('Network.enable') }) + + it('errors if reconnecting fails', async () => { + criStub._notifier.on = sinon.stub() + criStub.close.throws(new Error('could not reconnect')) + + await getClient() + // @ts-ignore + await criStub._notifier.on.withArgs('disconnect').args[0][1]() + + expect(onError).to.be.called + + const error = onError.lastCall.args[0] + + expect(error.messageMarkdown).to.equal('There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser.') + expect(error.isFatalApiErr).to.be.true + }) }) }) diff --git a/packages/server/test/unit/browsers/privileged-channel_spec.js b/packages/server/test/unit/browsers/privileged-channel_spec.js new file mode 100644 index 0000000000..0337072a3e --- /dev/null +++ b/packages/server/test/unit/browsers/privileged-channel_spec.js @@ -0,0 +1,180 @@ +require('../../spec_helper') + +const path = require('path') +const { fs } = require('../../../lib/util/fs') + +describe('privileged channel', () => { + let runPrivilegedChannel + let win + + beforeEach(async () => { + const secureChannelScript = (await fs.readFileAsync(path.join(__dirname, '..', '..', '..', 'lib', 'privileged-commands', 'privileged-channel.js'))).toString() + const ErrorStub = function (message) { + return new Error(message) + } + + ErrorStub.captureStackTrace = sinon.stub() + + // need to pull out the methods like this so when they're overwritten + // in the tests, they don't mess up the actual globals since the test + // runner itself relies on them + win = { + Array: { prototype: { + filter: Array.prototype.filter, + includes: Array.prototype.includes, + map: Array.prototype.map, + } }, + Error: ErrorStub, + Cypress: { + on () {}, + }, + fetch: sinon.stub().resolves(), + Function: { prototype: { + toString: Function.prototype.toString, + } }, + JSON: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + String: { prototype: { + includes: String.prototype.includes, + replace: String.prototype.replace, + split: String.prototype.split, + } }, + } + + runPrivilegedChannel = () => { + return eval(`${secureChannelScript}`)({ + browserFamily: 'chromium', + isSpecBridge: false, + key: '1234', + namespace: '__cypress', + scripts: JSON.stringify(['cypress/e2e/spec.cy.js']), + url: 'http://localhost:12345/__cypress/tests?p=cypress/integration/some-spec.js', + win, + }) + } + }) + + describe('overwriting native objects and methods has no effect', () => { + it('Error', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.Error = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.Error).not.to.be.called + }) + + it('Error.captureStackTrace', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.Error.captureStackTrace = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.Error.captureStackTrace).not.to.be.called + }) + + it('Array.prototype.filter', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.Array.prototype.filter = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.Array.prototype.filter).not.to.be.called + }) + + it('Array.prototype.includes', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.Array.prototype.includes = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.Array.prototype.includes).not.to.be.called + }) + + it('Array.prototype.map', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.Array.prototype.map = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.Array.prototype.map).not.to.be.called + }) + + it('String.prototype.split', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.String.prototype.split = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.String.prototype.split).not.to.be.called + }) + + it('String.prototype.replace', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.String.prototype.replace = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.String.prototype.replace).not.to.be.called + }) + + it('String.prototype.includes', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.String.prototype.includes = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.String.prototype.includes).not.to.be.called + }) + + it('Function.prototype.toString', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.Function.prototype.toString = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.Function.prototype.toString).not.to.be.called + }) + + it('fetch', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.fetch = sinon.stub().resolves() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.fetch).not.to.be.called + }) + + it('JSON.stringify', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.JSON.stringify = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.JSON.stringify).not.to.be.called + }) + + it('JSON.parse', () => { + const { onCommandInvocation } = runPrivilegedChannel() + + win.JSON.parse = sinon.stub() + + onCommandInvocation({ name: 'task', args: [] }) + + expect(win.JSON.parse).not.to.be.called + }) + }) +}) diff --git a/packages/server/test/unit/files_spec.js b/packages/server/test/unit/files_spec.js index 59a37bac65..13e47d79e3 100644 --- a/packages/server/test/unit/files_spec.js +++ b/packages/server/test/unit/files_spec.js @@ -28,7 +28,7 @@ describe('lib/files', () => { context('#readFile', () => { it('returns contents and full file path', function () { - return files.readFile(this.projectRoot, 'tests/_fixtures/message.txt').then(({ contents, filePath }) => { + return files.readFile(this.projectRoot, { file: 'tests/_fixtures/message.txt' }).then(({ contents, filePath }) => { expect(contents).to.eq('foobarbaz') expect(filePath).to.include('/cy-projects/todos/tests/_fixtures/message.txt') @@ -36,26 +36,26 @@ describe('lib/files', () => { }) it('returns uses utf8 by default', function () { - return files.readFile(this.projectRoot, 'tests/_fixtures/ascii.foo').then(({ contents }) => { + return files.readFile(this.projectRoot, { file: 'tests/_fixtures/ascii.foo' }).then(({ contents }) => { expect(contents).to.eq('\n') }) }) it('uses encoding specified in options', function () { - return files.readFile(this.projectRoot, 'tests/_fixtures/ascii.foo', { encoding: 'ascii' }).then(({ contents }) => { + return files.readFile(this.projectRoot, { file: 'tests/_fixtures/ascii.foo', encoding: 'ascii' }).then(({ contents }) => { expect(contents).to.eq('o#?\n') }) }) // https://github.com/cypress-io/cypress/issues/1558 it('explicit null encoding is sent to driver as a Buffer', function () { - return files.readFile(this.projectRoot, 'tests/_fixtures/ascii.foo', { encoding: null }).then(({ contents }) => { + return files.readFile(this.projectRoot, { file: 'tests/_fixtures/ascii.foo', encoding: null }).then(({ contents }) => { expect(contents).to.eql(Buffer.from('\n')) }) }) it('parses json to valid JS object', function () { - return files.readFile(this.projectRoot, 'tests/_fixtures/users.json').then(({ contents }) => { + return files.readFile(this.projectRoot, { file: 'tests/_fixtures/users.json' }).then(({ contents }) => { expect(contents).to.eql([ { id: 1, @@ -71,8 +71,8 @@ describe('lib/files', () => { context('#writeFile', () => { it('writes the file\'s contents and returns contents and full file path', function () { - return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'foo').then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents, filePath }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'foo' }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents, filePath }) => { expect(contents).to.equal('foo') expect(filePath).to.include('/cy-projects/todos/.projects/write_file.txt') @@ -81,8 +81,8 @@ describe('lib/files', () => { }) it('uses encoding specified in options', function () { - return files.writeFile(this.projectRoot, '.projects/write_file.txt', '', { encoding: 'ascii' }).then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: '', encoding: 'ascii' }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => { expect(contents).to.equal('�') }) }) @@ -90,20 +90,20 @@ describe('lib/files', () => { // https://github.com/cypress-io/cypress/issues/1558 it('explicit null encoding is written exactly as received', function () { - return files.writeFile(this.projectRoot, '.projects/write_file.txt', Buffer.from(''), { encoding: null }).then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt', { encoding: null }).then(({ contents }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: Buffer.from(''), encoding: null }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt', encoding: null }).then(({ contents }) => { expect(contents).to.eql(Buffer.from('')) }) }) }) it('overwrites existing file by default', function () { - return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'foo').then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'foo' }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => { expect(contents).to.equal('foo') - return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'bar').then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'bar' }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => { expect(contents).to.equal('bar') }) }) @@ -112,12 +112,12 @@ describe('lib/files', () => { }) it('appends content to file when specified', function () { - return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'foo').then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'foo' }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => { expect(contents).to.equal('foo') - return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'bar', { flag: 'a+' }).then(() => { - return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => { + return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'bar', flag: 'a+' }).then(() => { + return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => { expect(contents).to.equal('foobar') }) }) diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index 9a558a059c..ca88243e8f 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -11,7 +11,6 @@ const errors = require('../../lib/errors') const { SocketE2E } = require('../../lib/socket-e2e') const { ServerE2E } = require('../../lib/server-e2e') const { Automation } = require('../../lib/automation') -const exec = require('../../lib/exec') const preprocessor = require('../../lib/plugins/preprocessor') const { fs } = require('../../lib/util/fs') const session = require('../../lib/session') @@ -108,11 +107,11 @@ describe('lib/socket', () => { foo.bar.baz = foo - // going to stub exec here just so we have something that we can + // stubbing session#getSession here just so we have something that we can // control the resolved value of - sinon.stub(exec, 'run').resolves(foo) + sinon.stub(session, 'getSession').resolves(foo) - return this.client.emit('backend:request', 'exec', 'quuz', (res) => { + return this.client.emit('backend:request', 'get:session', 'quuz', (res) => { expect(res.response).to.deep.eq(foo) return done() @@ -512,33 +511,6 @@ describe('lib/socket', () => { }) }) - context('on(backend:request, exec)', () => { - it('calls exec#run with project root and options', function (done) { - const run = sinon.stub(exec, 'run').returns(Promise.resolve('Desktop Music Pictures')) - - return this.client.emit('backend:request', 'exec', { cmd: 'ls' }, (resp) => { - expect(run).to.be.calledWith(this.cfg.projectRoot, { cmd: 'ls' }) - expect(resp.response).to.eq('Desktop Music Pictures') - - return done() - }) - }) - - it('errors when execution fails, passing through timedOut', function (done) { - const error = new Error('command not found: lsd') - - error.timedOut = true - sinon.stub(exec, 'run').rejects(error) - - return this.client.emit('backend:request', 'exec', { cmd: 'lsd' }, (resp) => { - expect(resp.error.message).to.equal('command not found: lsd') - expect(resp.error.timedOut).to.be.true - - return done() - }) - }) - }) - context('on(backend:request, firefox:force:gc)', () => { it('calls firefoxUtil#collectGarbage', function (done) { sinon.stub(firefoxUtil, 'collectGarbage').resolves() diff --git a/system-tests/__snapshots__/cdp_spec.ts.js b/system-tests/__snapshots__/cdp_spec.ts.js deleted file mode 100644 index ae59f97236..0000000000 --- a/system-tests/__snapshots__/cdp_spec.ts.js +++ /dev/null @@ -1,26 +0,0 @@ -exports['e2e cdp / handles disconnections as expected'] = ` - -==================================================================================================== - - (Run Starting) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Cypress: 1.2.3 │ - │ Browser: FooBrowser 88 │ - │ Specs: 1 found (spec.cy.ts) │ - │ Searched: cypress/e2e/spec.cy.ts │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - -──────────────────────────────────────────────────────────────────────────────────────────────────── - - Running: spec.cy.ts (1 of 1) - - - e2e remote debugging disconnect - ✓ reconnects as expected -There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser. - -Error: connect ECONNREFUSED 127.0.0.1:7777 - [stack trace lines] -` diff --git a/system-tests/project-fixtures/runner-specs/cypress/e2e/errors/readfile.cy.js b/system-tests/project-fixtures/runner-specs/cypress/e2e/errors/readfile.cy.js deleted file mode 100644 index e91de8ac83..0000000000 --- a/system-tests/project-fixtures/runner-specs/cypress/e2e/errors/readfile.cy.js +++ /dev/null @@ -1,5 +0,0 @@ -describe('cy.readFile', () => { - it('existence failure', () => { - cy.readFile('does-not-exist', { timeout: 100 }) - }) -}) diff --git a/system-tests/projects/remote-debugging-disconnect/cypress/e2e/spec.cy.ts b/system-tests/projects/remote-debugging-disconnect/cypress/e2e/spec.cy.ts index 4cbd7c14e9..6f0bcb5bbd 100644 --- a/system-tests/projects/remote-debugging-disconnect/cypress/e2e/spec.cy.ts +++ b/system-tests/projects/remote-debugging-disconnect/cypress/e2e/spec.cy.ts @@ -31,14 +31,4 @@ describe('e2e remote debugging disconnect', () => { currentConnectionCount: 1, }) }) - - it('errors if CDP connection cannot be reestablished', () => { - cy.task('destroy:server') - cy.task('kill:active:connections') - - cy.then(() => { - // this will cause a project-level error once we realize we can't talk to CDP anymore - return callAutomation() - }) - }) }) diff --git a/system-tests/projects/remote-debugging-disconnect/plugins.js b/system-tests/projects/remote-debugging-disconnect/plugins.js index 14e150c013..de1f7515de 100644 --- a/system-tests/projects/remote-debugging-disconnect/plugins.js +++ b/system-tests/projects/remote-debugging-disconnect/plugins.js @@ -81,12 +81,6 @@ module.exports = (on, config) => { return null }, - 'destroy:server' () { - console.error('closing server') - server.close() - - return null - }, }) diff --git a/system-tests/test/cdp_spec.ts b/system-tests/test/cdp_spec.ts index 72f92fc768..82f7e1f817 100644 --- a/system-tests/test/cdp_spec.ts +++ b/system-tests/test/cdp_spec.ts @@ -15,24 +15,10 @@ describe('e2e cdp', function () { restoreEnv() }) - // NOTE: this test takes almost a minute and is largely redundant with protocol_spec - systemTests.it.skip('fails when remote debugging port cannot be connected to', { - project: 'remote-debugging-port-removed', - spec: 'spec.cy.ts', - browser: 'chrome', - expectedExitCode: 1, - }) - // https://github.com/cypress-io/cypress/issues/5685 systemTests.it('handles disconnections as expected', { project: 'remote-debugging-disconnect', spec: 'spec.cy.ts', browser: 'chrome', - expectedExitCode: 1, - snapshot: true, - onStdout: (stdout) => { - // the location of this warning is non-deterministic - return stdout.replace('The automation client disconnected. Cannot continue running tests.\n', '') - }, }) }) diff --git a/yarn.lock b/yarn.lock index 43769776ff..e33e5f28b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -144,16 +144,11 @@ resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.5.2.tgz#8c2d931ff927be0ebe740169874a3d4004ab414b" integrity sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA== -"@antfu/utils@^0.7.0": +"@antfu/utils@^0.7.0", "@antfu/utils@^0.7.2": version "0.7.4" resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.4.tgz#b1c11b95f89f13842204d3d83de01e10bb9257db" integrity sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ== -"@antfu/utils@^0.7.2": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.2.tgz#3bb6f37a6b188056fe9e2f363b6aa735ed65d7ca" - integrity sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g== - "@ardatan/aggregate-error@0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz#fe6924771ea40fc98dc7a7045c2e872dc8527609" From d76d4e1ecfe4d7617347dd91a26007fc2735c604 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Tue, 20 Jun 2023 10:21:50 -0600 Subject: [PATCH 4/4] chore: 12.15.0 release (#27087) --- cli/CHANGELOG.md | 2 +- cli/types/cypress.d.ts | 5 +++-- package.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index cfc8e01bf0..0354ef7a75 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,7 +1,7 @@ ## 12.15.0 -_Released 06/20/2023 (PENDING)_ +_Released 06/20/2023_ **Features:** diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index c59a94f1f7..4fcad80e7c 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3062,7 +3062,7 @@ declare namespace Cypress { * specified will remain in the response headers. * * Please see the documentation for more information. - * @see https://on.cypress.io/configuration#experimentalCspAllowList + * @see https://on.cypress.io/experiments#Experimental-CSP-Allow-List * @default false */ experimentalCspAllowList: boolean | experimentalCspAllowedDirectives[], @@ -3077,7 +3077,7 @@ declare namespace Cypress { * Please see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity. * This option has no impact on experimentalSourceRewriting and is only used with the * non-experimental source rewriter. - * @see https://on.cypress.io/configuration#experimentalModifyObstructiveThirdPartyCode + * @see https://on.cypress.io/experiments#Configuration */ experimentalModifyObstructiveThirdPartyCode: boolean /** @@ -3087,6 +3087,7 @@ declare namespace Cypress { * navigations, and will require the use of cy.origin(). This option takes an array of * strings/string globs. * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/domain + * @see https://on.cypress.io/experiments#Experimental-Skip-Domain-Injection * @default null */ experimentalSkipDomainInjection: string[] | null diff --git a/package.json b/package.json index f8b22384f3..9f7bcef975 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "12.14.0", + "version": "12.15.0", "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": {