-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
index.ts
235 lines (196 loc) · 8.95 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
224
225
226
227
228
229
230
231
232
233
234
235
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 normalizeOrigin = (originOrDomain) => {
let origin = originOrDomain
// If just a domain, convert it to an origin by adding the protocol
if (!reHttp.test(originOrDomain)) {
origin = `https://${originOrDomain}`
}
return $Location.normalize(origin)
}
export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State, config: Cypress.InternalConfig) {
let timeoutId
const communicator = Cypress.primaryOriginCommunicator
communicator.on('delaying:html', (request) => {
// when a cross origin request 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 a cross origin request
cy.isAnticipatingCrossOriginResponseFor(request)
const location = $Location.create(request.href)
// If this event has occurred while a switchToDomain command is running with
// the same origin policy, do not set the time out and allow switchToDomain
// to handle the ready for domain event
if (cy.state('currentActiveOriginPolicy') === location.originPolicy) {
return
}
// 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 origin.
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
// origin 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(normalizeOrigin(originOrDomain)).toString()
const location = $Location.create(url)
validator.validateLocation(location, originOrDomain)
const originPolicy = location.originPolicy
// This is intentionally not reset after leaving the switchToDomain command.
cy.state('latestActiveOriginPolicy', originPolicy)
// This is set while IN the switchToDomain command.
cy.state('currentActiveOriginPolicy', originPolicy)
return new Bluebird((resolve, reject, onCancel) => {
const cleanup = () => {
cy.state('currentActiveOriginPolicy', undefined)
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
// origin 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 origin 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, specBridgeOriginPolicy) => {
if (specBridgeOriginPolicy === 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 origin page loads, send along the
// user-specified callback to run in that origin
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: Cypress.state('hookId'),
switchToDomainBaseUrl: location.origin,
parentOriginPolicies: [cy.getRemoteLocation('originPolicy')],
isStable: Cypress.state('isStable'),
autOrigin: Cypress.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)
})
},
})
}