diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 3c9a8c1657ed..ab9bf59bd188 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3019,6 +3019,16 @@ declare namespace Cypress { * @see https://on.cypress.io/configuration#experimentalModifyObstructiveThirdPartyCode */ experimentalModifyObstructiveThirdPartyCode: boolean + /** + * Disables setting document.domain to the applications super domain on injection. + * This experiment is to be used for sites that do not work with setting document.domain + * due to cross-origin issues. Enabling this option no longer allows for default subdomain + * 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 + * @default null + */ + experimentalSkipDomainInjection: string[] | null /** * Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm. * @default false diff --git a/packages/config/__snapshots__/index.spec.ts.js b/packages/config/__snapshots__/index.spec.ts.js index c6eb06cf4c76..0f2daa44b22e 100644 --- a/packages/config/__snapshots__/index.spec.ts.js +++ b/packages/config/__snapshots__/index.spec.ts.js @@ -38,6 +38,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1 'experimentalInteractiveRunEvents': false, 'experimentalRunAllSpecs': false, 'experimentalModifyObstructiveThirdPartyCode': false, + 'experimentalSkipDomainInjection': null, 'experimentalOriginDependencies': false, 'experimentalSourceRewriting': false, 'experimentalSingleTabRunMode': false, @@ -123,6 +124,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f 'experimentalInteractiveRunEvents': false, 'experimentalRunAllSpecs': false, 'experimentalModifyObstructiveThirdPartyCode': false, + 'experimentalSkipDomainInjection': null, 'experimentalOriginDependencies': false, 'experimentalSourceRewriting': false, 'experimentalSingleTabRunMode': false, @@ -204,6 +206,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key 'experimentalInteractiveRunEvents', 'experimentalRunAllSpecs', 'experimentalModifyObstructiveThirdPartyCode', + 'experimentalSkipDomainInjection', 'experimentalOriginDependencies', 'experimentalSourceRewriting', 'experimentalSingleTabRunMode', diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index 763cd2be343e..664ec35040fd 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -215,6 +215,12 @@ const driverConfigOptions: Array = [ validation: validate.isBoolean, isExperimental: true, requireRestartOnChange: 'server', + }, { + name: 'experimentalSkipDomainInjection', + defaultValue: null, + validation: validate.isNullOrArrayOfStrings, + isExperimental: true, + requireRestartOnChange: 'server', }, { name: 'experimentalOriginDependencies', defaultValue: false, @@ -679,6 +685,12 @@ export const breakingRootOptions: Array = [ isWarning: false, testingTypes: ['e2e'], }, + { + name: 'experimentalSkipDomainInjection', + errorKey: 'EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY', + isWarning: false, + testingTypes: ['e2e'], + }, ] export const testingTypeBreakingOptions: { e2e: Array, component: Array } = { @@ -720,5 +732,10 @@ export const testingTypeBreakingOptions: { e2e: Array, component errorKey: 'EXPERIMENTAL_ORIGIN_DEPENDENCIES_E2E_ONLY', isWarning: false, }, + { + name: 'experimentalSkipDomainInjection', + errorKey: 'EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY', + isWarning: false, + }, ], } diff --git a/packages/config/src/validation.ts b/packages/config/src/validation.ts index 589f502e93a7..8f023db35515 100644 --- a/packages/config/src/validation.ts +++ b/packages/config/src/validation.ts @@ -328,3 +328,11 @@ export function isStringOrArrayOfStrings (key: string, value: any): ErrResult | return errMsg(key, value, 'a string or an array of strings') } + +export function isNullOrArrayOfStrings (key: string, value: any): ErrResult | true { + if (_.isNull(value) || isArrayOfStrings(value)) { + return true + } + + return errMsg(key, value, 'an array of strings or null') +} diff --git a/packages/config/test/project/utils.spec.ts b/packages/config/test/project/utils.spec.ts index 450c96b1b2b2..82d3d128bc3e 100644 --- a/packages/config/test/project/utils.spec.ts +++ b/packages/config/test/project/utils.spec.ts @@ -1052,6 +1052,7 @@ describe('config/src/project/utils', () => { env: {}, execTimeout: { value: 60000, from: 'default' }, experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, + experimentalSkipDomainInjection: { value: null, from: 'default' }, experimentalFetchPolyfill: { value: false, from: 'default' }, experimentalInteractiveRunEvents: { value: false, from: 'default' }, experimentalOriginDependencies: { value: false, from: 'default' }, @@ -1147,6 +1148,7 @@ describe('config/src/project/utils', () => { downloadsFolder: { value: 'cypress/downloads', from: 'default' }, execTimeout: { value: 60000, from: 'default' }, experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, + experimentalSkipDomainInjection: { value: null, from: 'default' }, experimentalFetchPolyfill: { value: false, from: 'default' }, experimentalInteractiveRunEvents: { value: false, from: 'default' }, experimentalOriginDependencies: { value: false, from: 'default' }, diff --git a/packages/driver/src/cy/commands/origin/validator.ts b/packages/driver/src/cy/commands/origin/validator.ts index eead76728ff7..35bef1d09793 100644 --- a/packages/driver/src/cy/commands/origin/validator.ts +++ b/packages/driver/src/cy/commands/origin/validator.ts @@ -85,10 +85,14 @@ export class Validator { } // Users would be better off not using cy.origin if the origin is part of the same super domain. - if (cors.urlMatchesPolicyBasedOnDomain(originLocation.href, specHref)) { + if (cors.urlMatchesPolicyBasedOnDomain(originLocation.href, specHref, { + skipDomainInjectionForDomains: Cypress.config('experimentalSkipDomainInjection'), + })) { // this._isSameSuperDomainOriginWithExceptions({ originLocation, specLocation })) { - const policy = cors.policyForDomain(originLocation.href) + const policy = cors.policyForDomain(originLocation.href, { + skipDomainInjectionForDomains: Cypress.config('experimentalSkipDomainInjection'), + }) $errUtils.throwErrByPath('origin.invalid_url_argument_same_origin', { onFail: this.log, diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 1dfee6488b4e..2d0ded980238 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -44,6 +44,7 @@ import { PrimaryOriginCommunicator, SpecBridgeCommunicator } from './cross-origi import { setupAutEventHandlers } from './cypress/aut_event_handlers' import type { CachedTestState } from '@packages/types' +import * as cors from '@packages/network/lib/cors' const debug = debugFn('cypress:driver:cypress') @@ -182,7 +183,11 @@ class $Cypress { // set domainName but allow us to turn // off this feature in testing - if (domainName && config.testingType === 'e2e') { + const shouldInjectDocumentDomain = cors.shouldInjectDocumentDomain(window.location.origin, { + skipDomainInjectionForDomains: config.experimentalSkipDomainInjection, + }) + + if (domainName && config.testingType === 'e2e' && shouldInjectDocumentDomain) { document.domain = domainName } diff --git a/packages/driver/src/cypress/ensure.ts b/packages/driver/src/cypress/ensure.ts index d398a8ee0e68..32fe258cf90f 100644 --- a/packages/driver/src/cypress/ensure.ts +++ b/packages/driver/src/cypress/ensure.ts @@ -255,6 +255,7 @@ const commandCanCommunicateWithAUT = (cy: $Cy, err?): boolean => { const crossOriginCommandError = $errUtils.errByPath('miscellaneous.cross_origin_command', { commandOrigin: window.location.origin, autOrigin: cy.state('autLocation').origin, + isSkipDomainInjectionEnabled: !!Cypress.config('experimentalSkipDomainInjection'), }) if (err) { diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index dfb61ace1c9c..9ceb81b290eb 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -912,13 +912,15 @@ export default { return `Timed out retrying after ${ms}ms: ` }, test_stopped: 'Cypress test was stopped while running this command.', - cross_origin_command ({ commandOrigin, autOrigin }) { + cross_origin_command ({ commandOrigin, autOrigin, isSkipDomainInjectionEnabled }) { return { message: stripIndent`\ The command was expected to run against origin \`${commandOrigin}\` but the application is at origin \`${autOrigin}\`. This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly. - + ${isSkipDomainInjectionEnabled ? ` + If \`experimentalSkipDomainInjection\` is enabled for this domain, a ${cmd('origin')} command is required. + ` : ''} Using ${cmd('origin')} to wrap the commands run on \`${autOrigin}\` will likely fix this issue. \`cy.origin('${autOrigin}', () => {\` diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 605f5c069ff6..e203995cdf90 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1185,6 +1185,20 @@ export const AllCypressErrors = { ${fmt.code(code)}` }, + EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY: () => { + const code = errPartial` + { + e2e: { + experimentalSkipDomainInjection: ['*.salesforce.com', '*.force.com', '*.google.com', 'google.com'] + }, + }` + + return errTemplate`\ + The ${fmt.highlight(`experimentalSkipDomainInjection`)} experiment is currently only supported for End to End Testing and must be configured as an e2e testing type property: ${fmt.highlightSecondary(`e2e.experimentalSkipDomainInjection`)}. + The suggested values are only a recommendation. + + ${fmt.code(code)}` + }, FIREFOX_GC_INTERVAL_REMOVED: () => { return errTemplate`\ The ${fmt.highlight(`firefoxGcInterval`)} configuration option was removed in ${fmt.cypressVersion(`8.0.0`)}. It was introduced to work around a bug in Firefox 79 and below. diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index cfb8eb68f623..967a5048838e 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -1268,5 +1268,11 @@ describe('visual error templates', () => { default: [], } }, + + EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY: () => { + return { + default: [], + } + }, }) }) diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 1157242a77f6..fc3a10673dab 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -800,6 +800,7 @@ enum ErrorTypeEnum { EXPERIMENTAL_SINGLE_TAB_RUN_MODE EXPERIMENTAL_STUDIO_E2E_ONLY EXPERIMENTAL_STUDIO_REMOVED + EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY EXTENSION_NOT_LOADED FIREFOX_COULD_NOT_CONNECT FIREFOX_GC_INTERVAL_REMOVED diff --git a/packages/network/lib/cors.ts b/packages/network/lib/cors.ts index 139eac42dc48..7a3e553c875d 100644 --- a/packages/network/lib/cors.ts +++ b/packages/network/lib/cors.ts @@ -1,4 +1,5 @@ import _ from 'lodash' +import minimatch from 'minimatch' import * as uri from './uri' import debugModule from 'debug' import _parseDomain from '@cypress/parse-domain' @@ -11,6 +12,8 @@ const debug = debugModule('cypress:network:cors') // match IP addresses or anything following the last . const customTldsRe = /(^[\d\.]+$|\.[^\.]+$)/ +// TODO: if experimentalSkipDomainInjection plans to go GA, we can likely lump this strictSameOriginDomains +// into that config option by default. @see https://github.com/cypress-io/cypress/issues/25317 const strictSameOriginDomains = Object.freeze(['google.com']) export function getSuperDomain (url) { @@ -158,15 +161,58 @@ export const urlSameSiteMatch = (frameUrl: string, topUrl: string): boolean => { }) } +/** + * @param url - the url to check the policy against. + * @param arrayOfStringOrGlobPatterns - an array of url strings or globs to match against + * @returns {boolean} - whether or not a match was found + */ +const doesUrlHostnameMatchGlobArray = (url: string, arrayOfStringOrGlobPatterns: string[]): boolean => { + let { hostname } = uri.parse(url) + + return !!arrayOfStringOrGlobPatterns.find((globPattern) => { + return minimatch(hostname || '', globPattern) + }) +} + /** * Returns the policy that will be used for the specified url. * @param url - the url to check the policy against. + * @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined. * @returns a Policy string. */ -export const policyForDomain = (url: string): Policy => { +export const policyForDomain = (url: string, opts?: { + skipDomainInjectionForDomains: string[] | null | undefined +}): Policy => { const obj = parseUrlIntoHostProtocolDomainTldPort(url) + let shouldUseSameOriginPolicy = strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) + + if (!shouldUseSameOriginPolicy && _.isArray(opts?.skipDomainInjectionForDomains)) { + // if the strict same origins matches came up false, we should check the user provided config value for skipDomainInjectionForDomains, if one exists + shouldUseSameOriginPolicy = doesUrlHostnameMatchGlobArray(url, opts?.skipDomainInjectionForDomains as string[]) + } + + return shouldUseSameOriginPolicy ? + 'same-origin' : + 'same-super-domain-origin' +} + +/** + * @param url - The url to check for injection + * @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined. + * @returns {boolean} whether or not document.domain should be injected solely based on the url. + */ +export const shouldInjectDocumentDomain = (url: string, opts?: { + skipDomainInjectionForDomains: string[] | null +}) => { + // When determining if we want to injection document domain, + // We need to make sure the experimentalSkipDomainInjection feature flag is off. + // If on, we need to make sure the glob pattern doesn't exist in the array so we cover possible intersections (google). + if (_.isArray(opts?.skipDomainInjectionForDomains)) { + // if we match the glob, we want to return false + return !doesUrlHostnameMatchGlobArray(url, opts?.skipDomainInjectionForDomains as string[]) + } - return strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) ? 'same-origin' : 'same-super-domain-origin' + return true } /** @@ -175,11 +221,14 @@ export const policyForDomain = (url: string): Policy => { * in which case the policy is 'same-origin' * @param frameUrl - The url you are testing the policy for. * @param topUrl - The url you are testing the policy in context of. + * @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined. * @returns boolean, true if matching, false if not. */ -export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string): boolean => { +export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string, opts?: { + skipDomainInjectionForDomains: string[] | null +}): boolean => { return urlMatchesPolicy({ - policy: policyForDomain(frameUrl), + policy: policyForDomain(frameUrl, opts), frameUrl, topUrl, }) @@ -191,11 +240,13 @@ export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string): * in which case the policy is 'same-origin' * @param frameUrl - The url you are testing the policy for. * @param topProps - The props of the url you are testing the policy in context of. + * @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined. * @returns boolean, true if matching, false if not. */ -export const urlMatchesPolicyBasedOnDomainProps = (frameUrl: string, topProps: ParsedHostWithProtocolAndHost): boolean => { - const obj = parseUrlIntoHostProtocolDomainTldPort(frameUrl) - const policy = strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) ? 'same-origin' : 'same-super-domain-origin' +export const urlMatchesPolicyBasedOnDomainProps = (frameUrl: string, topProps: ParsedHostWithProtocolAndHost, opts?: { + skipDomainInjectionForDomains: string[] +}): boolean => { + const policy = policyForDomain(frameUrl, opts) return urlMatchesPolicyProps({ policy, diff --git a/packages/network/package.json b/packages/network/package.json index 886cdca638b6..bb6283bea0ce 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -20,6 +20,7 @@ "debug": "^4.3.2", "fs-extra": "9.1.0", "lodash": "^4.17.21", + "minimatch": "3.0.5", "node-forge": "1.3.0", "proxy-from-env": "1.0.0" }, diff --git a/packages/network/test/unit/cors_spec.ts b/packages/network/test/unit/cors_spec.ts index 348e65ddd2f3..1614e20aec9e 100644 --- a/packages/network/test/unit/cors_spec.ts +++ b/packages/network/test/unit/cors_spec.ts @@ -657,4 +657,86 @@ describe('lib/cors', () => { expect(cors.getOrigin('http://www.app.herokuapp.com:8080')).to.equal('http://www.app.herokuapp.com:8080') }) }) + + context('.policyForDomain', () => { + const recommendedSameOriginPolicyUrlGlobs = ['*.salesforce.com', '*.force.com', '*.google.com', 'google.com'] + + context('returns "same-origin" for google domains', () => { + it('accounts.google.com', () => { + expect(cors.policyForDomain('https://accounts.google.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-origin') + }) + + it('www.google.com', () => { + expect(cors.policyForDomain('https://www.google.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-origin') + }) + }) + + context('returns "same-origin" for salesforce domains', () => { + it('https://the-host.develop.lightning.force.com', () => { + expect(cors.policyForDomain('https://the-host.develop.lightning.force.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-origin') + }) + + it('https://the-host.develop.my.salesforce.com', () => { + expect(cors.policyForDomain('https://the-host.develop.my.salesforce.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-origin') + }) + + it('https://the-host.develop.file.force.com', () => { + expect(cors.policyForDomain('https://the-host.develop.file.force.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-origin') + }) + + it('https://the-host.develop.my.salesforce.com', () => { + expect(cors.policyForDomain('https://the-host.develop.my.salesforce.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-origin') + }) + }) + + describe('returns "same-super-domain-origin" for non exception urls', () => { + it('www.cypress.io', () => { + expect(cors.policyForDomain('http://www.cypress.io', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-super-domain-origin') + }) + + it('docs.cypress.io', () => { + expect(cors.policyForDomain('http://docs.cypress.io', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-super-domain-origin') + }) + + it('stackoverflow.com', () => { + expect(cors.policyForDomain('https://stackoverflow.com', { + skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs, + })).to.equal('same-super-domain-origin') + }) + }) + }) + + context('.shouldInjectDocumentDomain', () => { + it('returns false when "skipDomainInjectionForDomains" is configured and contains a matching blob pattern ', () => { + expect(cors.shouldInjectDocumentDomain('http://www.cypress.io', { + skipDomainInjectionForDomains: ['*.cypress.io'], + })).to.be.false + }) + + it('returns true when "skipDomainInjectionForDomains" exists, but doesn\'t contain a matching glob pattern', () => { + expect(cors.shouldInjectDocumentDomain('http://www.cypress.io', { + skipDomainInjectionForDomains: ['*.foobar.com'], + })).to.be.true + }) + + it('returns true otherwise', () => { + expect(cors.shouldInjectDocumentDomain('http://www.cypress.io')).to.be.true + }) + }) }) diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts index 4e676290d028..ecd1c20a6dbc 100644 --- a/packages/proxy/lib/http/request-middleware.ts +++ b/packages/proxy/lib/http/request-middleware.ts @@ -162,8 +162,8 @@ const MaybeEndRequestWithBufferedResponse: RequestMiddleware = function () { if (buffer) { this.debug('ending request with buffered response') - // NOTE: Only inject fullCrossOrigin here if experimental is on and - // the super domain origins do not match in order to keep parity with cypress application reloads + + // NOTE: Only inject fullCrossOrigin here if the super domain origins do not match in order to keep parity with cypress application reloads this.res.wantsInjection = buffer.urlDoesNotMatchPolicyBasedOnDomain ? 'fullCrossOrigin' : 'full' return this.onResponse(buffer.response, buffer.stream) diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 83b4e7a7290c..918764cbf59d 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -55,9 +55,11 @@ function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer, return 'latin1' } -function reqMatchesPolicyBasedOnDomain (req: CypressIncomingRequest, remoteState) { +function reqMatchesPolicyBasedOnDomain (req: CypressIncomingRequest, remoteState, skipDomainInjectionForDomains) { if (remoteState.strategy === 'http') { - return cors.urlMatchesPolicyBasedOnDomainProps(req.proxiedUrl, remoteState.props) + return cors.urlMatchesPolicyBasedOnDomainProps(req.proxiedUrl, remoteState.props, { + skipDomainInjectionForDomains, + }) } if (remoteState.strategy === 'file') { @@ -250,7 +252,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { this.debug('determine injection') - const isReqMatchSuperDomainOrigin = reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.current()) + const isReqMatchSuperDomainOrigin = reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.current(), this.config.experimentalSkipDomainInjection) const getInjectionLevel = () => { if (this.incomingRes.headers['x-cypress-file-server-error'] && !this.res.isInitial) { this.debug('- partial injection (x-cypress-file-server-error)') @@ -259,7 +261,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { } // NOTE: Only inject fullCrossOrigin if the super domain origins do not match in order to keep parity with cypress application reloads - const urlDoesNotMatchPolicyBasedOnDomain = !reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.getPrimary()) + const urlDoesNotMatchPolicyBasedOnDomain = !reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.getPrimary(), this.config.experimentalSkipDomainInjection) const isAUTFrame = this.req.isAUTFrame const isHTMLLike = isHTML || isRenderedHTML @@ -544,6 +546,9 @@ const MaybeInjectHtml: ResponseMiddleware = function () { isNotJavascript: !resContentTypeIsJavaScript(this.incomingRes), useAstSourceRewriting: this.config.experimentalSourceRewriting, modifyObstructiveThirdPartyCode: this.config.experimentalModifyObstructiveThirdPartyCode && !this.remoteStates.isPrimarySuperDomainOrigin(this.req.proxiedUrl), + shouldInjectDocumentDomain: cors.shouldInjectDocumentDomain(this.req.proxiedUrl, { + skipDomainInjectionForDomains: this.config.experimentalSkipDomainInjection, + }), modifyObstructiveCode: this.config.modifyObstructiveCode, url: this.req.proxiedUrl, deferSourceMapRewrite: this.deferSourceMapRewrite, diff --git a/packages/proxy/lib/http/util/inject.ts b/packages/proxy/lib/http/util/inject.ts index f6c20bfe64b9..830143173c6f 100644 --- a/packages/proxy/lib/http/util/inject.ts +++ b/packages/proxy/lib/http/util/inject.ts @@ -2,25 +2,42 @@ import { oneLine } from 'common-tags' import { getRunnerInjectionContents, getRunnerCrossOriginInjectionContents } from '@packages/resolve-dist' import type { AutomationCookie } from '@packages/server/lib/automation/cookies' +interface InjectionOpts { + shouldInjectDocumentDomain: boolean +} interface FullCrossOriginOpts { modifyObstructiveThirdPartyCode: boolean modifyObstructiveCode: boolean simulatedCookies: AutomationCookie[] } -export function partial (domain) { +export function partial (domain, options: InjectionOpts) { + let documentDomainInjection = `document.domain = '${domain}';` + + if (!options.shouldInjectDocumentDomain) { + documentDomainInjection = '' + } + + // 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` ` } -export function full (domain) { +export function full (domain, options: InjectionOpts) { return getRunnerInjectionContents().then((contents) => { + let documentDomainInjection = `document.domain = '${domain}';` + + if (!options.shouldInjectDocumentDomain) { + documentDomainInjection = '' + } + return oneLine` @@ -28,12 +45,18 @@ export function full (domain) { }) } -export async function fullCrossOrigin (domain, options: FullCrossOriginOpts) { +export async function fullCrossOrigin (domain, options: InjectionOpts & FullCrossOriginOpts) { const contents = await getRunnerCrossOriginInjectionContents() + let documentDomainInjection = `document.domain = '${domain}';` + + if (!options.shouldInjectDocumentDomain) { + documentDomainInjection = '' + } + return oneLine` diff --git a/packages/server/lib/routes-e2e.ts b/packages/server/lib/routes-e2e.ts index 68b65ed60c88..cadce9c5eec4 100644 --- a/packages/server/lib/routes-e2e.ts +++ b/packages/server/lib/routes-e2e.ts @@ -105,7 +105,7 @@ export const createRoutesE2E = ({ // @see https://github.com/cypress-io/cypress/issues/25010 res.setHeader('Origin-Agent-Cluster', '?0') - files.handleCrossOriginIframe(req, res, config.namespace) + files.handleCrossOriginIframe(req, res, config) }) return routesE2E diff --git a/packages/server/lib/server-e2e.ts b/packages/server/lib/server-e2e.ts index 704d320fd563..7e83a5d79fd2 100644 --- a/packages/server/lib/server-e2e.ts +++ b/packages/server/lib/server-e2e.ts @@ -45,6 +45,8 @@ const isResponseHtml = function (contentType, responseBuffer) { export class ServerE2E extends ServerBase { private _urlResolver: Bluebird> | null + // the initialization of this variable is only precautionary as the actual config value is applied when the server is created + private skipDomainInjectionForDomains: string[] | null = null constructor () { super() @@ -58,10 +60,10 @@ export class ServerE2E extends ServerBase { createServer (app, config, onWarning): Bluebird<[number, WarningErr?]> { return new Bluebird((resolve, reject) => { - const { port, fileServerFolder, socketIoRoute, baseUrl } = config + const { port, fileServerFolder, socketIoRoute, baseUrl, experimentalSkipDomainInjection } = config this._server = this._createHttpServer(app) - + this.skipDomainInjectionForDomains = experimentalSkipDomainInjection const onError = (err) => { // if the server bombs before starting // and the err no is EADDRINUSE @@ -308,7 +310,9 @@ export class ServerE2E extends ServerBase { // TODO: think about moving this logic back into the frontend so that the driver can be in control // of when to buffer and set the remote state if (isOk && details.isHtml) { - const urlDoesNotMatchPolicyBasedOnDomain = options.hasAlreadyVisitedUrl && !cors.urlMatchesPolicyBasedOnDomain(primaryRemoteState.origin, newUrl || '') || options.isFromSpecBridge + const urlDoesNotMatchPolicyBasedOnDomain = options.hasAlreadyVisitedUrl + && !cors.urlMatchesPolicyBasedOnDomain(primaryRemoteState.origin, newUrl || '', { skipDomainInjectionForDomains: this.skipDomainInjectionForDomains }) + || options.isFromSpecBridge if (!handlingLocalFile) { this._remoteStates.set(newUrl as string, options, !urlDoesNotMatchPolicyBasedOnDomain) diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 9a202258d16c..f42b567b1418 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -2199,7 +2199,7 @@ describe('Routes', () => { context('content injection', () => { beforeEach(function () { - return this.setup('http://www.google.com') + return this.setup('http://www.cypress.io') }) it('injects when head has attributes', async function () { @@ -2212,7 +2212,7 @@ describe('Routes', () => { const injection = await getRunnerInjectionContents() const contents = removeWhitespace(Fixtures.get('server/expected_head_inject.html').replace('{{injection}}', injection)) const res = await this.rp({ - url: 'http://www.google.com/bar', + url: 'http://www.cypress.io/bar', headers: { 'Cookie': '__cypress.initial=true', }, @@ -2234,7 +2234,7 @@ describe('Routes', () => { const contents = removeWhitespace(Fixtures.get('server/expected_no_head_tag_inject.html').replace('{{injection}}', injection)) const res = await this.rp({ - url: 'http://www.google.com/bar', + url: 'http://www.cypress.io/bar', headers: { 'Cookie': '__cypress.initial=true', }, @@ -2253,7 +2253,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/bar', + url: 'http://www.cypress.io/bar', headers: { 'Cookie': '__cypress.initial=true', }, @@ -2261,7 +2261,7 @@ describe('Routes', () => { .then((res) => { expect(res.statusCode).to.eq(200) - expect(res.body).to.include(' hello from bar! ') + expect(res.body).to.eq(' hello from bar! ') }) }) @@ -2397,7 +2397,7 @@ describe('Routes', () => { .get('/bar') .reply(302, undefined, { // redirect us to google.com! - 'Location': 'http://www.google.com/foo', + 'Location': 'http://www.cypress.io/foo', }) nock(this.server.remoteStates.current().origin) @@ -2407,14 +2407,14 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/bar', + url: 'http://www.cypress.io/bar', headers: { 'Cookie': '__cypress.initial=true', }, }) .then((res) => { expect(res.statusCode).to.eq(302) - expect(res.headers['location']).to.eq('http://www.google.com/foo') + expect(res.headers['location']).to.eq('http://www.cypress.io/foo') expect(res.headers['set-cookie']).to.match(/initial=true/) return this.rp(res.headers['location']) @@ -2437,7 +2437,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/elements.html', + url: 'http://www.cypress.io/elements.html', headers: { 'Cookie': '__cypress.initial=true', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', @@ -2446,7 +2446,7 @@ describe('Routes', () => { .then((res) => { expect(res.statusCode).to.eq(200) - expect(res.body).to.include('document.domain = \'google.com\';') + expect(res.body).to.include('document.domain = \'cypress.io\';') }) }) @@ -2479,7 +2479,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/bar', + url: 'http://www.cypress.io/bar', headers: { 'Cookie': '__cypress.initial=false', }, @@ -2534,10 +2534,10 @@ describe('Routes', () => { }) it('injects even on 5xx responses', function () { - return this.setup('https://www.google.com') + return this.setup('https://www.cypress.io') .then(() => { this.server.onRequest((req, res) => { - return nock('https://www.google.com') + return nock('https://www.cypress.io') .get('/') .reply(500, 'google', { 'Content-Type': 'text/html', @@ -2545,7 +2545,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'https://www.google.com/', + url: 'https://www.cypress.io/', headers: { 'Accept': 'text/html, application/xhtml+xml, */*', }, @@ -2553,7 +2553,7 @@ describe('Routes', () => { .then((res) => { expect(res.statusCode).to.eq(500) - expect(res.body).to.include('document.domain = \'google.com\'') + expect(res.body).to.include('document.domain = \'cypress.io\'') }) }) }) @@ -2624,7 +2624,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/iframe', + url: 'http://www.cypress.io/iframe', headers: { 'Cookie': '__cypress.initial=false', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', @@ -2635,19 +2635,19 @@ describe('Routes', () => { const body = cleanResponseBody(res.body) - expect(body).to.eq(' ') + expect(body).to.eq(' ') }) }) it('does not inject document.domain on matching super domains but different subdomain - when the domain is set to strict same origin (google)', function () { - nock('http://mail.google.com') + nock('http://www.google.com') .get('/iframe') .reply(200, '', { 'Content-Type': 'text/html', }) return this.rp({ - url: 'http://mail.google.com/iframe', + url: 'http://www.google.com/iframe', headers: { 'Cookie': '__cypress.initial=false', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', @@ -2694,7 +2694,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/json', + url: 'http://www.cypress.io/json', json: true, headers: { 'Cookie': '__cypress.initial=false', @@ -2734,7 +2734,7 @@ describe('Routes', () => { .reply(200, { foo: 'bar' }) return this.rp({ - url: 'http://www.google.com/json', + url: 'http://www.cypress.io/json', headers: { 'Cookie': '__cypress.initial=true', 'Accept': 'application/json', @@ -2757,7 +2757,7 @@ describe('Routes', () => { }) return this.rp({ - url: 'http://www.google.com/iframe', + url: 'http://www.cypress.io/iframe', headers: { 'Cookie': '__cypress.initial=false', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', @@ -2788,7 +2788,7 @@ describe('Routes', () => { headers['Accept'] = type return this.rp({ - url: 'http://www.google.com/iframe', + url: 'http://www.cypress.io/iframe', headers, }) .then((res) => { diff --git a/packages/server/test/integration/server_spec.js b/packages/server/test/integration/server_spec.js index ae39a68996fe..98fc563543ce 100644 --- a/packages/server/test/integration/server_spec.js +++ b/packages/server/test/integration/server_spec.js @@ -823,7 +823,7 @@ describe('Server', () => { }) it('can serve non 2xx status code requests when option set', function () { - nock('http://google.com') + nock('http://cypress.io') .matchHeader('user-agent', 'foobarbaz') .matchHeader('accept', 'text/html,*/*') .get('/foo') @@ -837,29 +837,29 @@ describe('Server', () => { headers['user-agent'] = 'foobarbaz' - return this.server._onResolveUrl('http://google.com/foo', headers, this.automationRequest, { failOnStatusCode: false }) + return this.server._onResolveUrl('http://cypress.io/foo', headers, this.automationRequest, { failOnStatusCode: false }) .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, isPrimarySuperDomainOrigin: true, isHtml: true, contentType: 'text/html', - url: 'http://google.com/foo', - originalUrl: 'http://google.com/foo', + url: 'http://cypress.io/foo', + originalUrl: 'http://cypress.io/foo', status: 404, statusText: 'Not Found', redirects: [], cookies: [], }) }).then(() => { - return this.rp('http://google.com/foo') + return this.rp('http://cypress.io/foo') .then((res) => { expect(res.statusCode).to.eq(404) expect(res.headers['set-cookie']).not.to.match(/initial=;/) expect(res.headers['x-foo-bar']).to.eq('true') expect(res.headers['cache-control']).to.eq('no-cache, no-store, must-revalidate') expect(res.body).to.include('content') - expect(res.body).to.include('document.domain = \'google.com\'') + expect(res.body).to.include('document.domain = \'cypress.io\'') expect(res.body).to.include('.action("app:window:before:load",window)') expect(res.body).to.include('content') @@ -1132,7 +1132,7 @@ describe('Server', () => { }) it('can go from file -> http -> file', function () { - nock('http://www.google.com') + nock('http://www.cypress.io') .get('/') .reply(200, 'content page', { 'Content-Type': 'text/html', @@ -1159,35 +1159,35 @@ describe('Server', () => { expect(res.statusCode).to.eq(200) }) }).then(() => { - return this.server._onResolveUrl('http://www.google.com/', {}, this.automationRequest) + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest) }).then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, isPrimarySuperDomainOrigin: true, isHtml: true, contentType: 'text/html', - url: 'http://www.google.com/', - originalUrl: 'http://www.google.com/', + url: 'http://www.cypress.io/', + originalUrl: 'http://www.cypress.io/', status: 200, statusText: 'OK', redirects: [], cookies: [], }) }).then(() => { - return this.rp('http://www.google.com/') + return this.rp('http://www.cypress.io/') .then((res) => { expect(res.statusCode).to.eq(200) }) }).then(() => { expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, - origin: 'http://www.google.com', + origin: 'http://www.cypress.io', strategy: 'http', - domainName: 'google.com', + domainName: 'cypress.io', fileServer: null, props: { - domain: 'google', - tld: 'com', + domain: 'cypress', + tld: 'io', port: '80', subdomain: 'www', protocol: 'http:', @@ -1228,50 +1228,50 @@ describe('Server', () => { }) it('can go from http -> file -> http', function () { - nock('http://www.google.com') + nock('http://www.cypress.io') .get('/') - .reply(200, 'google', { + .reply(200, 'cypress', { 'Content-Type': 'text/html', }) .get('/') - .reply(200, 'google', { + .reply(200, 'cypress', { 'Content-Type': 'text/html', }) - return this.server._onResolveUrl('http://www.google.com/', {}, this.automationRequest) + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest) .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, isPrimarySuperDomainOrigin: true, isHtml: true, contentType: 'text/html', - url: 'http://www.google.com/', - originalUrl: 'http://www.google.com/', + url: 'http://www.cypress.io/', + originalUrl: 'http://www.cypress.io/', status: 200, statusText: 'OK', redirects: [], cookies: [], }) }).then(() => { - return this.rp('http://www.google.com/') + return this.rp('http://www.cypress.io/') .then((res) => { expect(res.statusCode).to.eq(200) expect(res.body).to.include('document.domain') - expect(res.body).to.include('google.com') + expect(res.body).to.include('cypress.io') expect(res.body).to.include('.action("app:window:before:load",window)') - expect(res.body).to.include('google') + expect(res.body).to.include('cypress') }) }).then(() => { expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, - origin: 'http://www.google.com', + origin: 'http://www.cypress.io', strategy: 'http', - domainName: 'google.com', + domainName: 'cypress.io', fileServer: null, props: { - domain: 'google', - tld: 'com', + domain: 'cypress', + tld: 'io', port: '80', subdomain: 'www', protocol: 'http:', @@ -1313,40 +1313,40 @@ describe('Server', () => { props: null, }) }).then(() => { - return this.server._onResolveUrl('http://www.google.com/', {}, this.automationRequest) + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest) .then((obj = {}) => { return expectToEqDetails(obj, { isOkStatusCode: true, isPrimarySuperDomainOrigin: true, isHtml: true, contentType: 'text/html', - url: 'http://www.google.com/', - originalUrl: 'http://www.google.com/', + url: 'http://www.cypress.io/', + originalUrl: 'http://www.cypress.io/', status: 200, statusText: 'OK', redirects: [], cookies: [], }) }).then(() => { - return this.rp('http://www.google.com/') + return this.rp('http://www.cypress.io/') .then((res) => { expect(res.statusCode).to.eq(200) expect(res.body).to.include('document.domain') - expect(res.body).to.include('google.com') + expect(res.body).to.include('cypress.io') expect(res.body).to.include('.action("app:window:before:load",window)') - expect(res.body).to.include('google') + expect(res.body).to.include('cypress') }) }).then(() => { expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, - origin: 'http://www.google.com', + origin: 'http://www.cypress.io', strategy: 'http', - domainName: 'google.com', + domainName: 'cypress.io', fileServer: null, props: { - domain: 'google', - tld: 'com', + domain: 'cypress', + tld: 'io', port: '80', subdomain: 'www', protocol: 'http:', diff --git a/packages/server/test/support/fixtures/server/expected_head_inject.html b/packages/server/test/support/fixtures/server/expected_head_inject.html index 7574cf2b730f..7848f4547c9b 100644 --- a/packages/server/test/support/fixtures/server/expected_head_inject.html +++ b/packages/server/test/support/fixtures/server/expected_head_inject.html @@ -1,7 +1,7 @@ diff --git a/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html b/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html index c825225cd8c8..226da67811da 100644 --- a/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html +++ b/packages/server/test/support/fixtures/server/expected_no_head_tag_inject.html @@ -1,7 +1,7 @@ diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index f059d9d69660..b67a7cfeff87 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -30,7 +30,7 @@ export interface FullConfig extends Partial - & Pick // TODO: Figure out how to type this better. + & Pick // TODO: Figure out how to type this better. export interface SettingsOptions { testingType?: 'component' |'e2e' diff --git a/system-tests/__snapshots__/experimental_skip_domain_injection_spec.ts.js b/system-tests/__snapshots__/experimental_skip_domain_injection_spec.ts.js new file mode 100644 index 000000000000..d1d3c53d857c --- /dev/null +++ b/system-tests/__snapshots__/experimental_skip_domain_injection_spec.ts.js @@ -0,0 +1,65 @@ +exports['e2e experimentalSkipDomainInjection=true / passes'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (experimental_skip_domain_injection.cy.ts) │ + │ Searched: cypress/e2e/experimental_skip_domain_injection.cy.ts │ + │ Experiments: experimentalSkipDomainInjection=*.foobar.com │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: experimental_skip_domain_injection.cy.ts (1 of 1) + + + expected behavior when experimentalSkipDomainInjection=true + ✓ Handles cross-site/cross-origin navigation the same way without the experimental flag enabled + ✓ errors appropriately when doing a sub domain navigation w/o cy.origin() + ✓ allows sub-domain navigations with the use of cy.origin() + + + 3 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 3 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: experimental_skip_domain_injection.cy.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/experimental_skip_domain_inject (X second) + ion.cy.ts.mp4 + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ experimental_skip_domain_injection. XX:XX 3 3 - - - │ + │ cy.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 3 3 - - - + + +` diff --git a/system-tests/projects/e2e/cypress/e2e/experimental_skip_domain_injection.cy.ts b/system-tests/projects/e2e/cypress/e2e/experimental_skip_domain_injection.cy.ts new file mode 100644 index 000000000000..fb906537dd7c --- /dev/null +++ b/system-tests/projects/e2e/cypress/e2e/experimental_skip_domain_injection.cy.ts @@ -0,0 +1,43 @@ +describe('expected behavior when experimentalSkipDomainInjection=true', () => { + it('Handles cross-site/cross-origin navigation the same way without the experimental flag enabled', () => { + cy.visit('/primary_origin.html') + cy.get('a[data-cy="cross_origin_secondary_link"]').click() + cy.origin('http://www.foobar.com:4466', () => { + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin') + }) + }) + + it('errors appropriately when doing a sub domain navigation w/o cy.origin()', () => { + const timeout = 500 + + cy.on('fail', (err) => { + expect(err.name).to.equal('CypressError') + expect(err.message).to.contain(`Timed out retrying after ${timeout}ms: The command was expected to run against origin \`http://app.foobar.com:4466\` but the application is at origin \`http://www.foobar.com:4466\`.`) + expect(err.message).to.contain('This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.') + expect(err.message).to.contain('Using `cy.origin()` to wrap the commands run on `http://www.foobar.com:4466` will likely fix this issue.') + expect(err.message).to.include(`cy.origin('http://www.foobar.com:4466', () => {\`\n\` \`\n\`})`) + expect(err.message).to.include('If `experimentalSkipDomainInjection` is enabled for this domain, a `cy.origin()` command is required.') + + // make sure that the secondary origin failures do NOT show up as spec failures or AUT failures + expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`) + expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`) + }) + + // with experimentalSkipDomainInjection, sub domain navigations require a cy.origin() block + cy.visit('http://app.foobar.com:4466/primary_origin.html') + cy.get('a[data-cy="cross_origin_secondary_link"]').click() + cy.get('[data-cy="dom-check"]', { + timeout, + }).should('have.text', 'From a secondary origin') + }) + + it('allows sub-domain navigations with the use of cy.origin()', () => { + cy.visit('http://app.foobar.com:4466/primary_origin.html') + cy.get('a[data-cy="cross_origin_secondary_link"]').click() + + // with experimentalSkipDomainInjection, sub domain navigations require a cy.origin() block + cy.origin('http://www.foobar.com:4466', () => { + cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin') + }) + }) +}) diff --git a/system-tests/test/experimental_skip_domain_injection_spec.ts b/system-tests/test/experimental_skip_domain_injection_spec.ts new file mode 100644 index 000000000000..1e4f6b4bab8d --- /dev/null +++ b/system-tests/test/experimental_skip_domain_injection_spec.ts @@ -0,0 +1,44 @@ +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('/primary_origin.html', (_, res) => { + res.sendFile(path.join(e2ePath, `primary_origin.html`)) + }) + + app.get('/secondary_origin.html', (_, res) => { + res.sendFile(path.join(e2ePath, `secondary_origin.html`)) + }) +} + +describe('e2e experimentalSkipDomainInjection=true', () => { + systemTests.setup({ + servers: [{ + port: 4466, + onServer, + }], + settings: { + hosts: { + '*.foobar.com': '127.0.0.1', + }, + e2e: {}, + }, + }) + + systemTests.it('passes', { + browser: '!webkit', // TODO(webkit): fix+unskip (needs multidomain support) + // keep the port the same to prevent issues with the snapshot + port: PORT, + spec: 'experimental_skip_domain_injection.cy.ts', + snapshot: true, + expectedExitCode: 0, + config: { + retries: 0, + experimentalSkipDomainInjection: ['*.foobar.com'], + }, + }) +})