-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
index.ts
223 lines (186 loc) · 8.29 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import Bluebird from 'bluebird'
import $errUtils from '../../cypress/error_utils'
import { Validator } from './validator'
import { createUnserializableSubjectProxy } from './unserializable_subject_proxy'
import { serializeRunnable } from './util'
import { preprocessConfig, preprocessEnv, syncConfigToCurrentDomain, syncEnvToCurrentDomain } from '../../util/config'
import { $Location } from '../../cypress/location'
import { LogUtils } from '../../cypress/log'
const reHttp = /^https?:\/\//
const normalizeDomain = (domain) => {
// add the protocol if it's not present
if (!reHttp.test(domain)) {
domain = `https://${domain}`
}
return $Location.normalize(domain)
}
export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State, config: Cypress.InternalConfig) {
let timeoutId
const communicator = Cypress.multiDomainCommunicator
communicator.on('delaying:html', (request) => {
// when a secondary domain is detected by the proxy, it holds it up
// to provide time for the spec bridge to be set up. normally, the queue
// will not continue until the page is stable, but this signals it to go
// ahead because we're anticipating multi-domain
// @ts-ignore
cy.isAnticipatingMultiDomainFor(request.href)
// If we haven't seen a switchToDomain and cleared the timeout within 300ms,
// go ahead and inform the server 'ready:for:domain' failed and to release the
// response. This typically happens during a redirect where the user does
// not have a switchToDomain for the intermediary domain.
timeoutId = setTimeout(() => {
Cypress.backend('ready:for:domain', { failed: true })
}, 300)
})
Commands.addAll({
switchToDomain<T> (originOrDomain: string, optionsOrFn: { args: T } | (() => {}), fn?: (args?: T) => {}) {
// store the invocation stack in the case that `switchToDomain` errors
communicator.userInvocationStack = state('current').get('userInvocationStack')
clearTimeout(timeoutId)
// this command runs for as long as the commands in the secondary
// domain run, so it can't have its own timeout
cy.clearTimeout()
if (!config('experimentalMultiDomain')) {
$errUtils.throwErrByPath('switchToDomain.experiment_not_enabled')
}
let options
let callbackFn
if (fn) {
callbackFn = fn
options = optionsOrFn
} else {
callbackFn = optionsOrFn
options = {
args: undefined,
}
}
const log = Cypress.log({
name: 'switchToDomain',
type: 'parent',
message: originOrDomain,
end: true,
})
const validator = new Validator({
log,
onFailure: () => {
Cypress.backend('ready:for:domain', { failed: true })
},
})
validator.validate({
callbackFn,
options,
originOrDomain,
})
// use URL to ensure unicode characters are correctly handled
const url = new URL(normalizeDomain(originOrDomain)).toString()
const location = $Location.create(url)
validator.validateLocation(location, originOrDomain)
const originPolicy = location.originPolicy
cy.state('latestActiveDomain', originPolicy)
return new Bluebird((resolve, reject, onCancel) => {
const cleanup = () => {
Cypress.backend('cross:origin:finished', location.originPolicy)
communicator.off('queue:finished', onQueueFinished)
communicator.off('sync:globals', onSyncGlobals)
}
onCancel && onCancel(() => {
cleanup()
})
const _resolve = ({ subject, unserializableSubjectType }) => {
cleanup()
resolve(unserializableSubjectType ? createUnserializableSubjectProxy(unserializableSubjectType) : subject)
}
const _reject = (err) => {
cleanup()
log.error(err)
reject(err)
}
const onQueueFinished = ({ err, subject, unserializableSubjectType }) => {
if (err) {
return _reject(err)
}
_resolve({ subject, unserializableSubjectType })
}
const onSyncGlobals = ({ config, env }) => {
syncConfigToCurrentDomain(config)
syncEnvToCurrentDomain(env)
}
communicator.once('sync:globals', onSyncGlobals)
communicator.once('ran:domain:fn', (details) => {
const { subject, unserializableSubjectType, err, finished } = details
// 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', { originPolicy: location.originPolicy })
if (err) {
return _reject(err)
}
// if there are not commands and a synchronous return from the callback,
// this resolves immediately
if (finished || subject || unserializableSubjectType) {
_resolve({ subject, unserializableSubjectType })
}
})
communicator.once('queue:finished', onQueueFinished)
// We don't unbind this even after queue:finished, because an async
// error could be thrown after the queue is done, but make sure not
// to stack up listeners on it after it's originally bound
if (!communicator.listeners('uncaught:error').length) {
communicator.once('uncaught:error', ({ err }) => {
// @ts-ignore
if (err?.name === 'CypressError') {
// This is a Cypress error thrown from the secondary domain after the command queue has finished, do not wrap it as a spec or app error.
cy.fail(err, { async: true })
} else {
// @ts-ignore
Cypress.runner.onSpecError('error')({ error: err })
}
})
}
// fired once the spec bridge is set up and ready to receive messages
communicator.once('bridge:ready', (_data, bridgeReadyDomain) => {
if (bridgeReadyDomain === originPolicy) {
// now that the spec bridge is ready, instantiate Cypress with the current app config and environment variables for initial sync when creating the instance
communicator.toSpecBridge(originPolicy, 'initialize:cypress', {
config: preprocessConfig(Cypress.config()),
env: preprocessEnv(Cypress.env()),
})
// once the secondary domain page loads, send along the
// user-specified callback to run in that domain
try {
communicator.toSpecBridge(originPolicy, 'run:domain:fn', {
args: options?.args || undefined,
fn: callbackFn.toString(),
// let the spec bridge version of Cypress know if config read-only values can be overwritten since window.top cannot be accessed in cross-origin iframes
// this should only be used for internal testing. Cast to boolean to guarantee serialization
// @ts-ignore
skipConfigValidation: !!window.top.__cySkipValidateConfig,
state: {
viewportWidth: Cypress.state('viewportWidth'),
viewportHeight: Cypress.state('viewportHeight'),
runnable: serializeRunnable(Cypress.state('runnable')),
duringUserTestExecution: Cypress.state('duringUserTestExecution'),
hookId: state('hookId'),
hasVisitedAboutBlank: state('hasVisitedAboutBlank'),
multiDomainBaseUrl: location.origin,
parentOrigins: [cy.getRemoteLocation('originPolicy')],
isStable: state('isStable'),
autOrigin: state('autOrigin'),
},
config: preprocessConfig(Cypress.config()),
env: preprocessEnv(Cypress.env()),
logCounter: LogUtils.getCounter(),
})
} catch (err: any) {
const wrappedErr = $errUtils.errByPath('switchToDomain.run_domain_fn_errored', {
error: err.message,
})
_reject(wrappedErr)
}
}
})
// this signals to the runner to create the spec bridge for the specified origin policy
communicator.emit('expect:domain', location)
})
},
})
}