diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index 691304d20ebb..adb2a4d62f03 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -303,6 +303,7 @@ const stabilityChanged = (Cypress, state, config, stable) => { configFile: Cypress.config('configFile'), message: err.message, originPolicy, + isExperimentalMultiDomain: Cypress.config('experimentalMultiDomain'), }, }) } catch (error) { diff --git a/packages/driver/src/cy/multi-domain/index.ts b/packages/driver/src/cy/multi-domain/index.ts index bdf4f3b84daf..45f9a1b69414 100644 --- a/packages/driver/src/cy/multi-domain/index.ts +++ b/packages/driver/src/cy/multi-domain/index.ts @@ -11,10 +11,11 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, // @ts-ignore const communicator = Cypress.multiDomainCommunicator - const sendReadyForDomain = () => { + const sendReadyForDomain = (hasHandler = false) => { // lets the proxy know to allow the response for the secondary // domain html through, so the page will finish loading - Cypress.backend('ready:for:domain') + // @ts-ignore + Cypress.backend('ready:for:domain', hasHandler) } communicator.on('delaying:html', () => { @@ -108,7 +109,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, communicator.once('ran:domain:fn', (details) => { const { subject, unserializableSubjectType, err, finished } = details - sendReadyForDomain() + sendReadyForDomain(true) if (err) { return _reject(err) diff --git a/packages/driver/src/cy/multi-domain/validator.ts b/packages/driver/src/cy/multi-domain/validator.ts index b1e681ddd4a9..0f44089642d4 100644 --- a/packages/driver/src/cy/multi-domain/validator.ts +++ b/packages/driver/src/cy/multi-domain/validator.ts @@ -7,7 +7,7 @@ import { isString } from 'lodash' export class Validator { log: typeof $Log - onFailure: () => {} + onFailure: (hasHandler: boolean) => {} constructor ({ log, onFailure }) { this.log = log @@ -16,7 +16,7 @@ export class Validator { validate ({ callbackFn, data, domain }) { if (!isString(domain) || domain !== 'localhost' && !isIP(domain) && !isValidDomain(domain, { allowUnicode: true, subdomain: false })) { - this.onFailure() + this.onFailure(false) $errUtils.throwErrByPath('switchToDomain.invalid_domain_argument', { onFail: this.log, @@ -25,7 +25,7 @@ export class Validator { } if (data && !Array.isArray(data)) { - this.onFailure() + this.onFailure(false) $errUtils.throwErrByPath('switchToDomain.invalid_data_argument', { onFail: this.log, @@ -34,7 +34,7 @@ export class Validator { } if (typeof callbackFn !== 'function') { - this.onFailure() + this.onFailure(false) $errUtils.throwErrByPath('switchToDomain.invalid_fn_argument', { onFail: this.log, diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 50ccc95c9678..afad7fd59d0e 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -942,7 +942,7 @@ export default { }, navigation: { - cross_origin ({ message, originPolicy, configFile }) { + cross_origin ({ message, originPolicy, configFile, isExperimentalMultiDomain }) { return { message: stripIndent`\ Cypress detected a cross origin error happened on page load: @@ -957,12 +957,17 @@ export default { A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different. - Cypress does not allow you to navigate to a different origin URL within a single test. - + ${isExperimentalMultiDomain ? `If cross origin navigation was intentional, ${cmd('switchToDomain')} needs to immediately follow a cross origin navigation event.` : ''} + + ${isExperimentalMultiDomain ? `Otherwise, ` : ''}Cypress does not allow you to navigate to a different origin URL within a single test. + + ${isExperimentalMultiDomain ? '' : ` You may need to restructure some of your test code to avoid this problem. - Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in ${formatConfigFile(configFile)}.`, - docsUrl: 'https://on.cypress.io/cross-origin-violation', + Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in ${formatConfigFile(configFile)}., + `}`, + // TODO: audit switchToDomain docs url + docsUrl: isExperimentalMultiDomain ? 'https://on.cypress.io/switch-to-domain' : 'https://on.cypress.io/cross-origin-violation', } }, timed_out ({ ms, configFile }) { diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 192b314d9ff1..b23c6b317c50 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -234,8 +234,9 @@ const MaybeDelayForMultiDomain: ResponseMiddleware = function () { if (this.config.experimentalMultiDomain && isCrossDomain && isAUTFrame && (isHTML || isRenderedHTML)) { this.debug('is cross-domain, delay until domain:ready event') - this.serverBus.once('ready:for:domain', () => { - this.debug('ready for domain, let it go') + this.serverBus.once('ready:for:domain', (hasHandler = false) => { + this.debug(`ready for domain. Does domain have a registered callback: ${hasHandler}.`) + this.res.locals.shouldInjectMultiDomain = hasHandler this.next() }) @@ -272,7 +273,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { const isHTML = resContentTypeIs(this.incomingRes, 'text/html') const isAUTFrame = this.req.isAUTFrame - if (this.config.experimentalMultiDomain && !isReqMatchOriginPolicy && isAUTFrame && (isHTML || isRenderedHTML)) { + if (this.config.experimentalMultiDomain && !isReqMatchOriginPolicy && isAUTFrame && (isHTML || isRenderedHTML) && this.res.locals.shouldInjectMultiDomain) { this.debug('- multi-domain injection') return 'fullMultiDomain' diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts index 89745ec567c4..e8f0be16c876 100644 --- a/packages/proxy/test/unit/http/response-middleware.spec.ts +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -182,6 +182,9 @@ describe('http/response-middleware', function () { req: { isAUTFrame: true, }, + res: { + locals: {}, + }, config: { experimentalMultiDomain: true, }, @@ -190,9 +193,10 @@ describe('http/response-middleware', function () { const promise = testMiddleware([MaybeDelayForMultiDomain], ctx) expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html') - ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]() + expect(ctx.res.locals.shouldInjectMultiDomain).to.be.false + return promise }) @@ -207,6 +211,9 @@ describe('http/response-middleware', function () { }, isAUTFrame: true, }, + res: { + locals: {}, + }, config: { experimentalMultiDomain: true, }, @@ -219,6 +226,34 @@ describe('http/response-middleware', function () { ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]() expect(ctx.res.wantsInjection).to.be.undefined + expect(ctx.res.locals.shouldInjectMultiDomain).to.be.false + + return promise + }) + + it('sets response locals "shouldInjectMultiDomain" to true if "ready:for:domain" emits true, indicating a bound domain callback exists', function () { + prepareContext({ + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + req: { + isAUTFrame: true, + }, + res: { + locals: {}, + }, + config: { + experimentalMultiDomain: true, + }, + }) + + const promise = testMiddleware([MaybeDelayForMultiDomain], ctx) + + ctx.serverBus.once.withArgs('ready:for:domain').args[0][1](true) + + expect(ctx.res.locals.shouldInjectMultiDomain).to.be.true return promise }) @@ -337,7 +372,7 @@ describe('http/response-middleware', function () { }) }) - it('injects "fullMultiDomain" when "experimentalMultiDomain" config flag is set to true for cross-domain html"', function () { + it('injects "fullMultiDomain" when "experimentalMultiDomain" config flag is set to true for cross-domain html and a callback handler exists', function () { prepareContext({ req: { proxiedUrl: 'http://foobar.com', @@ -345,6 +380,11 @@ describe('http/response-middleware', function () { cookies: {}, headers: {}, }, + res: { + locals: { + shouldInjectMultiDomain: true, + }, + }, incomingRes: { headers: { 'content-type': 'text/html', @@ -361,6 +401,44 @@ describe('http/response-middleware', function () { }) }) + it('injects default behavior when "experimentalMultiDomain" config flag is set to true for cross-domain html, but no callback handler exists for multi-domain', function () { + prepareContext({ + renderedHTMLOrigins: {}, + getRenderedHTMLOrigins () { + return this.renderedHTMLOrigins + }, + req: { + proxiedUrl: 'http://foobar.com', + isAUTFrame: true, + cookies: {}, + headers: { + 'accept': [ + 'text/html', + 'application/xhtml+xml', + ], + }, + }, + res: { + locals: { + shouldInjectMultiDomain: false, + }, + }, + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + config: { + experimentalMultiDomain: true, + }, + }) + + return testMiddleware([SetInjectionLevel], ctx) + .then(() => { + expect(ctx.res.wantsInjection).to.equal('partial') + }) + }) + it('performs full injection on initial AUT html origin', function () { prepareContext({ req: { diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index bf20dfef0334..27f18c84881b 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -140,8 +140,8 @@ export abstract class ServerBase { this._fileServer = null this._eventBus.on('cross:domain:delaying:html', () => { - this.socket.localBus.once('ready:for:domain', () => { - this._eventBus.emit('ready:for:domain') + this.socket.localBus.once('ready:for:domain', (hasRegisteredHandler) => { + this._eventBus.emit('ready:for:domain', hasRegisteredHandler) }) this.socket.toDriver('cross:domain:delaying:html') diff --git a/system-tests/__snapshots__/multi_domain_navigation_missing_callback.spec.ts.js b/system-tests/__snapshots__/multi_domain_navigation_missing_callback.spec.ts.js new file mode 100644 index 000000000000..59407eca5fca --- /dev/null +++ b/system-tests/__snapshots__/multi_domain_navigation_missing_callback.spec.ts.js @@ -0,0 +1,95 @@ +exports['e2e multi domain errors / captures the stack trace correctly for multi-domain errors to point users to their "switchToDomain" callback'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (multi_domain_navigation_missing_callback_spec.ts) │ + │ Searched: cypress/integration/multi_domain_navigation_missing_callback_spec.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: multi_domain_navigation_missing_callback_spec.ts (1 of 1) + + + multi-domain - navigation missing callback + ✓ passes since switchToDomain callback exists + 1) fails since switchToDomain callback is not registered for new cross origin domain + ✓ passes since switchToDomain callback exists + + + 2 passing + 1 failing + + 1) multi-domain - navigation missing callback + fails since switchToDomain callback is not registered for new cross origin domain: + CypressError: Cypress detected a cross origin error happened on page load: + + > [Cross origin error message] + +Before the page load, you were bound to the origin policy: + + > http://localhost:3500 + +A cross origin error happens when your application navigates to a new URL which does not match the origin policy above. + +A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different. + +If cross origin navigation was intentional, \`cy.switchToDomain()\` needs to immediately follow a cross origin navigation event. + +Otherwise, Cypress does not allow you to navigate to a different origin URL within a single test. + +https://on.cypress.io/switch-to-domain + [stack trace lines] + + + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 2 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: multi_domain_navigation_missing_callback_spec.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Screenshots) + + - /XXX/XXX/XXX/cypress/screenshots/multi_domain_navigation_missing_callback_spec.t (1280x720) + s/multi-domain - navigation missing callback -- fails since switchToDomain callb + ack is not registered for new cross origin domain (failed).png + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/multi_domain_navigation_missing (X second) + _callback_spec.ts.mp4 + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ multi_domain_navigation_missing_cal XX:XX 3 2 1 - - │ + │ lback_spec.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✖ 1 of 1 failed (100%) XX:XX 3 2 1 - - + + +` diff --git a/system-tests/projects/e2e/cypress/integration/multi_domain_navigation_missing_callback_spec.ts b/system-tests/projects/e2e/cypress/integration/multi_domain_navigation_missing_callback_spec.ts new file mode 100644 index 000000000000..ab9f89c422da --- /dev/null +++ b/system-tests/projects/e2e/cypress/integration/multi_domain_navigation_missing_callback_spec.ts @@ -0,0 +1,23 @@ +// @ts-ignore / session support is needed for visiting about:blank between tests +describe('multi-domain - navigation missing callback', { experimentalSessionSupport: true }, () => { + it('passes since switchToDomain callback exists', () => { + cy.visit('/multi_domain.html') + cy.get('a[data-cy="multi_domain_secondary_link"]').click() + + cy.switchToDomain('foobar.com', () => undefined) + }) + + it('fails since switchToDomain callback is not registered for new cross origin domain', () => { + cy.visit('/multi_domain.html') + cy.get('a[data-cy="multi_domain_secondary_link"]').click() + // give the test time for switchToDomain to timeout (currently is a 2 second timeout). Otherwise, request is cancelled + cy.wait(3000) + }) + + it('passes since switchToDomain callback exists', () => { + cy.visit('/multi_domain.html') + cy.get('a[data-cy="multi_domain_secondary_link"]').click() + + cy.switchToDomain('foobar.com', () => undefined) + }) +}) diff --git a/system-tests/projects/e2e/multi_domain.html b/system-tests/projects/e2e/multi_domain.html new file mode 100644 index 000000000000..8def8dca4e2b --- /dev/null +++ b/system-tests/projects/e2e/multi_domain.html @@ -0,0 +1,17 @@ + + + + + + +
+ Go to different domain: + http://www.foobar.com:4466/multi_domain_secondary.html +
+ + \ No newline at end of file diff --git a/system-tests/projects/e2e/multi_domain_secondary.html b/system-tests/projects/e2e/multi_domain_secondary.html new file mode 100644 index 000000000000..a905835ca0c4 --- /dev/null +++ b/system-tests/projects/e2e/multi_domain_secondary.html @@ -0,0 +1,41 @@ + + + + + +

From a secondary domain

+

+

+
+ +
+ + + + hashChange + /fixtures/multi_domain.html + + + + \ No newline at end of file diff --git a/system-tests/test/multi_domain_navigation_missing_callback.spec.ts b/system-tests/test/multi_domain_navigation_missing_callback.spec.ts new file mode 100644 index 000000000000..acdd49e3796f --- /dev/null +++ b/system-tests/test/multi_domain_navigation_missing_callback.spec.ts @@ -0,0 +1,45 @@ +import path from 'path' +import systemTests, { expect } from '../lib/system-tests' +import Fixtures from '../lib/fixtures' + +const e2ePath = Fixtures.projectPath('e2e') + +const PORT = 3500 +const onServer = function (app) { + app.get('/multi_domain_secondary.html', (_, res) => { + res.sendFile(path.join(e2ePath, `multi_domain_secondary.html`)) + }) +} + +describe('e2e multi domain errors', () => { + systemTests.setup({ + servers: [{ + port: 4466, + onServer, + }], + settings: { + hosts: { + '*.foobar.com': '127.0.0.1', + }, + }, + }) + + systemTests.it('captures the stack trace correctly for multi-domain errors to point users to their "switchToDomain" callback', { + // keep the port the same to prevent issues with the snapshot + port: PORT, + spec: 'multi_domain_navigation_missing_callback_spec.ts', + snapshot: true, + expectedExitCode: 1, + config: { + experimentalMultiDomain: true, + experimentalSessionSupport: true, + }, + async onRun (exec) { + const res = await exec() + + expect(res.stdout).to.contain('If cross origin navigation was intentional, `cy.switchToDomain()` needs to immediately follow a cross origin navigation event.') + expect(res.stdout).to.contain('Otherwise, Cypress does not allow you to navigate to a different origin URL within a single test.') + expect(res.stdout).to.contain('https://on.cypress.io/switch-to-domain') + }, + }) +}) diff --git a/system-tests/test/navigation_spec.ts b/system-tests/test/navigation_spec.ts index 1a0ff8bc9bba..f826fe1c9241 100644 --- a/system-tests/test/navigation_spec.ts +++ b/system-tests/test/navigation_spec.ts @@ -7,8 +7,6 @@ const onServer = function (app) { }) } -// FIXME: This partially solves https://github.com/cypress-io/cypress/issues/19632 but only when "experimentalMultiDomain" is false. -// TODO: This will be further solved by https://github.com/cypress-io/cypress/issues/20428 describe('e2e cross origin navigation', () => { systemTests.setup({ servers: [{