-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
index.ts
267 lines (218 loc) · 9.81 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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import Bluebird from 'bluebird'
import $errUtils from '../../../cypress/error_utils'
import $stackUtils from '../../../cypress/stack_utils'
import { Validator } from './validator'
import { createUnserializableSubjectProxy } from './unserializable_subject_proxy'
import { serializeRunnable } from './util'
import { preprocessConfig, preprocessEnv, syncConfigToCurrentOrigin, syncEnvToCurrentOrigin } from '../../../util/config'
import { $Location } from '../../../cypress/location'
import { LogUtils } from '../../../cypress/log'
import logGroup from '../../logGroup'
import type { StateFunc } from '../../../cypress/state'
import { runPrivilegedCommand } from '../../../util/privileged_channel'
const reHttp = /^https?:\/\//
const normalizeOrigin = (urlOrDomain) => {
let origin = urlOrDomain
// If just a domain, convert it to an origin by adding the protocol
if (!reHttp.test(urlOrDomain)) {
origin = `https://${urlOrDomain}`
}
return $Location.normalize(origin)
}
type OptionsOrFn<T> = { args: T } | (() => {})
type Fn<T> = (args?: T) => {}
export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: StateFunc, config: Cypress.InternalConfig) => {
const communicator = Cypress.primaryOriginCommunicator
Commands.addAll({
origin<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>, ...extras: never[]) {
if (Cypress.isBrowser('webkit')) {
return $errUtils.throwErrByPath('webkit.origin')
}
const userInvocationStack = state('current').get('userInvocationStack')
// store the invocation stack in the case that `cy.origin` errors
communicator.userInvocationStack = userInvocationStack
// this command runs for as long as the commands in the secondary
// origin run, so it can't have its own timeout except in the case where we're creating the spec bridge.
cy.clearTimeout()
let options
let callbackFn
const timeout = Cypress.config('defaultCommandTimeout')
if (fn) {
callbackFn = fn
options = optionsOrFn
} else {
callbackFn = optionsOrFn
options = {
args: undefined,
}
}
let log
logGroup(Cypress, {
name: 'origin',
type: 'parent',
message: urlOrDomain,
timeout,
// @ts-ignore TODO: revisit once log-grouping has more implementations
}, (_log) => {
log = _log
})
const validator = new Validator({
log,
})
validator.validate({
callbackFn,
options,
urlOrDomain,
})
// use URL to ensure unicode characters are correctly handled
const url = new URL(normalizeOrigin(urlOrDomain)).toString()
const location = $Location.create(url)
validator.validateLocation(location, urlOrDomain, window.location.href)
const origin = location.origin
// This is set while IN the cy.origin command.
cy.state('currentActiveOrigin', origin)
return new Bluebird((resolve, reject, onCancel) => {
const cleanup = (): void => {
cy.state('currentActiveOrigin', undefined)
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) => {
// Prevent cypress from trying to add the function to the error log
err.onFail = () => {}
cleanup()
log?.error(err)
reject(err)
}
const onQueueFinished = ({ err, subject, unserializableSubjectType }) => {
if (err) {
return _reject(err)
}
_resolve({ subject, unserializableSubjectType })
}
const onSyncGlobals = ({ config, env }) => {
syncConfigToCurrentOrigin(config)
syncEnvToCurrentOrigin(env)
}
communicator.once('sync:globals', onSyncGlobals)
communicator.once('ran:origin:fn', (details) => {
const { subject, unserializableSubjectType, err, finished } = details
if (err) {
if (err?.name === 'ReferenceError') {
const wrappedErr = $errUtils.errByPath('origin.ran_origin_fn_reference_error', {
error: err.message,
})
wrappedErr.name = err.name
wrappedErr.stack = $stackUtils.replacedStack(wrappedErr, err.stack)
return _reject(wrappedErr)
}
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 }) => {
cy.fail(err, { async: true })
})
}
// If the spec bridge isn't created in time, it likely failed and we shouldn't hang the test.
const timeoutId = setTimeout(() => {
_reject($errUtils.errByPath('origin.failed_to_create_spec_bridge'))
}, timeout)
// fired once the spec bridge is set up and ready to receive messages
communicator.once('bridge:ready', async (_data, { origin: specBridgeOrigin }) => {
if (specBridgeOrigin === origin) {
clearTimeout(timeoutId)
// 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(origin, 'initialize:cypress', {
config: preprocessConfig(Cypress.config()),
env: preprocessEnv(Cypress.env()),
})
// Attach the spec bridge to the window to be tested.
communicator.toSpecBridge(origin, 'attach:to:window')
const fn = _.isFunction(callbackFn) ? callbackFn.toString() : callbackFn
const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, config('projectRoot'))?.absoluteFile
try {
// origin is a privileged command, meaning it has to be invoked
// from the spec or support file
await runPrivilegedCommand({
commandName: 'origin',
cy,
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
options: {
specBridgeOrigin,
},
})
// once the secondary origin page loads, send along the
// user-specified callback to run in that origin
communicator.toSpecBridge(origin, 'run:origin:fn', {
args: options?.args || undefined,
fn,
file,
// 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'),
originCommandBaseUrl: location.href,
isStable: Cypress.state('isStable'),
autLocation: Cypress.state('autLocation')?.href,
crossOriginCookies: Cypress.state('crossOriginCookies'),
},
config: preprocessConfig(Cypress.config()),
env: preprocessEnv(Cypress.env()),
logCounter: LogUtils.getCounter(),
})
} catch (err: any) {
if (err.isNonSpec) {
return _reject($errUtils.errByPath('miscellaneous.non_spec_invocation', {
cmd: 'origin',
}))
}
const wrappedErr = $errUtils.errByPath('origin.run_origin_fn_errored', {
error: err.message,
})
wrappedErr.name = err.name
const stack = $stackUtils.replacedStack(wrappedErr, userInvocationStack)
// add the actual stack, since it might be useful for debugging
// the failure
wrappedErr.stack = $stackUtils.stackWithContentAppended({
appendToStack: {
title: 'From Cypress Internals',
content: $stackUtils.stackWithoutMessage(err.stack),
},
}, stack)
// @ts-ignore - This keeps Bluebird from messing with the stack.
// It tries to add a bunch of stuff that's not useful and ends up
// messing up the stack that we want on the error
wrappedErr.__stackCleaned__ = true
_reject(wrappedErr)
}
}
})
// this signals to the runner to create the spec bridge for the specified origin
communicator.emit('expect:origin', location)
})
},
})
}