Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Implement cross-domain sibling iframe #16708

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5498,6 +5498,8 @@ declare namespace Cypress {
message: any
/** Set to false if you want to control the finishing of the command in the log yourself */
autoEnd: boolean
/** Set to false if you want to control the finishing of the command in the log yourself */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference b/t this and autoEnd? same comment

i think you can also update this in packages/driver/types/internal-types.d.ts if you don't want it to be public

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can tell, autoEnd is redundant and we don't actually use it at all. The only instance of it is here:

options._log = Cypress.log({
snapshot: true,
autoEnd: false,
timeout: 0,

However, using autoEnd requires calling log.finish():

finish () {
// end our command since our subject
// has been resolved at this point
// unless its already been 'ended'
// or has been specifically told not to auto resolve
if (this._shouldAutoEnd()) {
return this.snapshot().end()
}
},

But I can't find any instances of calling log.finish() and the instance from above that uses autoEnd: true calls log.end(), which is the imperative way to end the snapshot, indicating it was never the intention to "auto-end" the log for cy.pause():

if (options.log) {
options._log.end()
}

The autoEnd option can probably be removed entirely, but that's outside the scope of this PR. I added the end option to the types since it's the one we actually use around the codebase for auto-ending a log. I guess this was the first time it was used in TypeScript though, so we hadn't noticed the omission before.

end: boolean
/** Return an object that will be printed in the dev tools console */
consoleProps(): ObjectLike
}
Expand Down
7 changes: 4 additions & 3 deletions packages/driver/cypress/fixtures/multidomain-aut.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
<h1>Multidomain AUT</h1>
<p>Some text in the cross-domain AUT</p>
<script>
setTimeout(() => {
window.onReady()
}, 500)
// currently this is hard-coded, but will be dynamically injected in the future
document.domain = 'localhost'

top.postMessage('app:cross:domain:window:load', '*')
</script>
</body>
</html>
3 changes: 1 addition & 2 deletions packages/driver/cypress/fixtures/multidomain.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<head>
</head>
<body>
<iframe src="http://localhost:3501/fixtures/multidomain-aut.html" width="800" height="800"></iframe>
<iframe src="http://localhost:3501/fixtures/multidomain-sibling.html" width="10" height="10"></iframe>
<a href="http://localhost:3501/fixtures/multidomain-aut.html">Go to localhost:3501</a>
</body>
</html>
22 changes: 19 additions & 3 deletions packages/driver/cypress/integration/e2e/multidomain_spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
// NOTE: this test only exists for manual verification as the
// multidomain bundle is a very incomplete work-in-progress
it('loads multidomain playground', () => {
// FIXME: Skip this for now since it's flaky
it.skip('verifies initial implementation of sibling iframe and switchToDomain', (done) => {
top.addEventListener('message', (event) => {
if (event.data && event.data.text) {
expect(event.data.text).to.equal('Some text in the cross-domain AUT')
expect(event.data.host).to.equal('localhost:3501')
done()
}
}, false)

cy.viewport(900, 300)
cy.visit('/fixtures/multidomain.html')
cy.get('a').click()
// @ts-ignore
cy.switchToDomain('localhost:3501', () => {
// @ts-ignore
cy.now('get', 'p').then(($el) => {
top.postMessage({ host: location.host, text: $el.text() }, '*')
})
})
})
14 changes: 14 additions & 0 deletions packages/driver/src/cy/multidomain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State) {
Commands.addAll({
switchToDomain (domain, fn) {
Cypress.log({
name: 'switchToDomain',
type: 'parent',
message: domain,
end: true,
})

Cypress.action('cy:cross:domain:message', 'run:in:domain', fn.toString())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn here will have the caveat that it doesn't have access to variables of parent lexical scopes, is this something that will be addressed or is it the plan to keep it that way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be addressed in future work. This is a bare-minimum version of this command just to get things rolling.

},
})
}
12 changes: 12 additions & 0 deletions packages/driver/src/cypress.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,18 @@ class $Cypress {
case 'cy:scrolled':
return this.emit('scrolled', ...args)

case 'app:cross:domain:window:load':
return this.emit('cross:domain:window:load', args[0])

case 'cy:switch:domain':
return this.emit('switch:domain', args[0])

case 'runner:cross:domain:driver:ready':
return this.emit('cross:domain:driver:ready')

case 'cy:cross:domain:message':
return this.emit('cross:domain:message', ...args)

case 'app:uncaught:exception':
return this.emitMap('uncaught:exception', ...args)

Expand Down
1 change: 1 addition & 0 deletions packages/driver/src/cypress/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const builtInCommands = [
require('../cy/commands/window'),
require('../cy/commands/xhr'),
require('../cy/net-stubbing').addCommand,
require('../cy/multidomain').addCommands,
]

const getTypeByPrevSubject = (prevSubject) => {
Expand Down
25 changes: 19 additions & 6 deletions packages/driver/src/cypress/cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,9 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
let onpl; let r

try {
setWindowDocumentProps(getContentWindow($autIframe), state)
const autWindow = getContentWindow($autIframe)

setWindowDocumentProps(autWindow, state)

// we may need to update the url now
urlNavigationEvent('load')
Expand All @@ -1037,14 +1039,25 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
// because they would have been automatically applied during
// onBeforeAppWindowLoad, but in the case where we visited
// about:blank in a visit, we do need these
contentWindowListeners(getContentWindow($autIframe))
contentWindowListeners(autWindow)

Cypress.action('app:window:load', state('window'))

// we are now stable again which is purposefully
// the last event we call here, to give our event
// listeners time to be invoked prior to moving on
return stability.isStable(true, 'load')
// FIXME: temporary hard-coded hack to get multidomain working
if (!autWindow?.location?.pathname?.includes('multidomain-aut')) {
// we are now stable again which is purposefully
// the last event we call here, to give our event
// listeners time to be invoked prior to moving on
return stability.isStable(true, 'load')
}

Cypress.once('cross:domain:window:load', () => {
Cypress.once('cross:domain:driver:ready', () => {
stability.isStable(true, 'load')
})

Cypress.action('cy:switch:domain', 'localhost:3501')
})
} catch (err) {
let e = err

Expand Down
14 changes: 13 additions & 1 deletion packages/driver/src/multidomain/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,19 @@ export const initialize = (autWindow) => {
timeout () {},
})

$Commands.create(Cypress, cy, Cypress.state)
$Commands.create(Cypress, cy, Cypress.state, Cypress.config)

window.addEventListener('message', (event) => {
if (event.data && event.data.message === 'run:in:domain') {
const stringifiedTestFn = event.data.data

autWindow.eval(`(${stringifiedTestFn})()`)
}
}, false)

top.postMessage('cross:domain:driver:ready', '*')

autWindow.cy = cy

return {
cy,
Expand Down
11 changes: 1 addition & 10 deletions packages/runner/multidomain/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import { initialize } from '@packages/driver/src/multidomain'

const autWindow = window.parent.frames[0]

const { cy } = initialize(autWindow)

autWindow.onReady = () => {
cy.now('get', 'p').then(($el) => {
// eslint-disable-next-line no-console
console.log('got the paragaph with text:', $el.text())
})
}
initialize(window.parent.frames[0])

/*

Expand Down
26 changes: 22 additions & 4 deletions packages/runner/src/iframe/iframes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export default class Iframes extends Component {

this.props.eventManager.on('print:selector:elements:to:console', this._printSelectorElementsToConsole)

this.props.eventManager.on('switch:domain', this._addCrossDomainIframe)

this._disposers.push(autorun(() => {
this.autIframe.toggleSelectorPlayground(selectorPlaygroundModel.isEnabled)
}))
Expand Down Expand Up @@ -148,14 +150,30 @@ export default class Iframes extends Component {

this.autIframe.showBlankContents()

this._addIframe({
$container,
id: `Your Spec: ${specSrc}`,
src: specSrc,
})

return $autIframe
}

_addCrossDomainIframe = (domain) => {
this._addIframe({
$container: $(this.refs.container),
id: `Cypress (${domain})`,
src: `http://${domain}/${this.props.config.namespace}/multidomain-iframes/${encodeURIComponent(domain)}`,
})
}

_addIframe ({ $container, id, src }) {
const $specIframe = $('<iframe />', {
id: `Your Spec: '${specSrc}'`,
id,
class: 'spec-iframe',
}).appendTo($container)

$specIframe.prop('src', specSrc)

return $autIframe
$specIframe.prop('src', src)
}

_toggleSnapshotHighlights = (snapshotProps) => {
Expand Down
20 changes: 19 additions & 1 deletion packages/runner/src/lib/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const driverToReporterEvents = 'paused before:firefox:force:gc after:firefox:for
const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed'.split(' ')
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed switch:domain'.split(' ')
const socketRerunEvents = 'runner:restart'.split(' ')
const socketToDriverEvents = 'net:event script:error'.split(' ')
const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ')
Expand Down Expand Up @@ -278,6 +278,20 @@ const eventManager = {
this._clearAllCookies()
this._setUnload()
})

top.addEventListener('message', (event) => {
switch (event.data) {
case 'app:cross:domain:window:load':
return Cypress.action('app:cross:domain:window:load')
case 'cross:domain:driver:ready':
this.crossDomainDriverWindow = event.source

return Cypress.action('runner:cross:domain:driver:ready')
default:
// eslint-disable-next-line no-console
console.log('Unknown postMessage:', event.data)
Comment on lines +291 to +292
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason to not throw here is because the AUT could emit a postMessage to top, and we shouldn't fail the test in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another bare-minimum implementation to get the ball rolling. We're planning more work on this in the future to put more thought into it and flesh it out more.

}
}, false)
},

start (config) {
Expand Down Expand Up @@ -471,6 +485,10 @@ const eventManager = {
studioRecorder.testFailed()
}
})

Cypress.on('cross:domain:message', (message, data) => {
this.crossDomainDriverWindow.postMessage({ message, data }, '*')
})
},

_runDriver (state) {
Expand Down
14 changes: 14 additions & 0 deletions packages/server/lib/controllers/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ module.exports = {
})
},

handleMultidomainIframe (req, res) {
const iframePath = cwd('lib', 'html', 'multidomain-iframe.html')
const domain = decodeURI(req.params.domain)

const iframeOptions = {
domain: 'localhost',
title: `Cypress for ${domain}`,
}

debug('multidomain iframe with options %o', iframeOptions)

res.render(iframePath, iframeOptions)
},

getSpecs (spec, config, extraOptions = {}) {
// when asking for all specs: spec = "__all"
// otherwise it is a relative spec filename like "integration/spec.js"
Expand Down
13 changes: 13 additions & 0 deletions packages/server/lib/html/multidomain-iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
</head>
<body>
<script type="text/javascript">
document.domain = '{{domain}}';
</script>
<script src="/__cypress/runner/cypress_multidomain_runner.js"></script>
</body>
</html>
7 changes: 7 additions & 0 deletions packages/server/lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ module.exports = ({ app, config, getRemoteState, networkProxy, project, onError
files.handleIframe(req, res, config, getRemoteState, extraOptions)
})

// routing for the dynamic iframe html
app.get('/__cypress/multidomain-iframes/:domain', (req, res) => {
debug('handling multidomain iframe for domain: %s', decodeURI(req.params.domain))

files.handleMultidomainIframe(req, res)
})

app.all('/__cypress/xhrs/*', (req, res, next) => {
xhrs.handle(req, res, config, next)
})
Expand Down