-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathorigin_fn.ts
More file actions
258 lines (216 loc) · 8.21 KB
/
origin_fn.ts
File metadata and controls
258 lines (216 loc) · 8.21 KB
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
import type { $Cy } from '../cypress/cy'
import $errUtils from '../cypress/error_utils'
import $utils from '../cypress/utils'
import { $Location } from '../cypress/location'
import { syncConfigToCurrentOrigin, syncEnvToCurrentOrigin, syncExposeToCurrentOrigin } from '../util/config'
import type { Runnable, Test } from 'mocha'
import { LogUtils } from '../cypress/log'
interface RunOriginFnState extends Cypress.ObjectLike {
originLogGroupLevel?: number
originLogGroupId?: string
}
interface RunOriginFnOptions {
config: Cypress.Config
args: any
env: Cypress.ObjectLike
expose: Cypress.ObjectLike
file?: string
fn: string
skipConfigValidation: boolean
state: RunOriginFnState
logCounter: number
}
interface serializedRunnable {
id: string
type: string
title: string
parent: serializedRunnable
ctx: {}
_currentRetry: number
_timeout: number
titlePath: string
}
interface GetFileResult {
contents?: string
error?: string
}
const rehydrateRunnable = (data: serializedRunnable): Runnable|Test => {
let runnable
if (data.type === 'test') {
runnable = Cypress.mocha.createTest(data.title, () => {})
} else {
runnable = new Cypress.mocha._mocha.Mocha.Runnable(data.title)
runnable.type = data.type
}
runnable.ctx = data.ctx
runnable.id = data.id
runnable._currentRetry = data._currentRetry
runnable._timeout = data._timeout
// Short circuit title path to avoid implementing it up the parent chain.
runnable.titlePath = () => {
return data.titlePath
}
if (data.parent) {
runnable.parent = rehydrateRunnable(data.parent)
}
// This is normally setup in the run command, but we don't call run.
// Any errors this would be reporting will already have been reported previously
runnable.callback = () => {}
return runnable
}
// Callback function handling / preprocessing for dependencies
// ---
// 1. If experimentalOriginDependencies is disabled or the string "Cypress.require"
// does not exist in the callback, just eval the callback as-is
// 2. Otherwise, we send it to the server
// 3. The server webpacks the callback to bundle in all the deps, then returns
// that bundle
// 4. Eval the callback like normal
const getCallbackFn = async (fn: string, file?: string) => {
if (
// @ts-expect-error
!Cypress.config('experimentalOriginDependencies')
|| !fn.includes('Cypress.require')
) {
return fn
}
// Since webpack will wrap everything up in a closure, we create a variable
// in the outer scope (see the return value below), assign the function to it
// in the inner scope, then call the function with the args
const callbackName = '__cypressCallback'
const response = await fetch('/__cypress/process-origin-callback', {
body: JSON.stringify({
file,
fn: `${callbackName} = ${fn};`,
projectRoot: Cypress.config('projectRoot'),
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const result = await response.json() as GetFileResult
if (result.error) {
$errUtils.throwErrByPath('origin.failed_to_get_callback', {
args: { error: result.error },
})
}
return `(args) => {
let ${callbackName};
${result.contents};
return ${callbackName}(args);
}`
}
export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
const reset = (state) => {
cy.reset({})
const stateUpdates = {
...state,
autLocation: $Location.create(state.autLocation),
redirectionCount: {}, // This is fine to set to an empty object, we want to refresh this count on each cy.origin command.
}
// Setup the runnable
stateUpdates.runnable = rehydrateRunnable(state.runnable)
// the viewport could've changed in the primary, so sync it up in the secondary
Cypress.primaryOriginCommunicator.emit('sync:viewport', { viewportWidth: state.viewportWidth, viewportHeight: state.viewportHeight })
// Update the state with the necessary values from the primary origin
cy.state(stateUpdates)
// Set the state ctx to the runnable ctx to ensure they remain in sync
cy.state('ctx', cy.state('runnable').ctx)
}
const setRunnableStateToPassed = () => {
// HACK: We're telling the runnable that it has passed to avoid a timeout
// on the last (empty) command. Normally this would be set inherently by
// running runnable.run() the test. Set this to passed regardless of the
// state of the test, the runnable isn't responsible for reporting success.
cy.state('runnable').state = 'passed'
}
Cypress.specBridgeCommunicator.on('run:origin:fn', async (options: RunOriginFnOptions) => {
const { config, args, env, expose, file, fn, state, skipConfigValidation, logCounter } = options
let queueFinished = false
reset(state)
cy.state('originLogGroupLevel', state?.originLogGroupLevel)
cy.state('originLogGroupId', state?.originLogGroupId)
// Set the counter for log ids
LogUtils.setCounter(logCounter)
// @ts-ignore
window.__cySkipValidateConfig = skipConfigValidation || false
// resync the config/env before running the origin:fn
syncConfigToCurrentOrigin(config)
syncExposeToCurrentOrigin(expose)
if (Cypress.config('allowCypressEnv')) {
syncEnvToCurrentOrigin(env)
}
const clearOriginLogGroupState = () => {
cy.state('originLogGroupLevel', undefined)
cy.state('originLogGroupId', undefined)
}
cy.state('onQueueEnd', () => {
queueFinished = true
setRunnableStateToPassed()
clearOriginLogGroupState()
Cypress.specBridgeCommunicator.toPrimary('queue:finished', {
subject: cy.subject(),
}, {
syncGlobals: true,
})
})
cy.state('onFail', (err) => {
setRunnableStateToPassed()
clearOriginLogGroupState()
if (queueFinished) {
// If the queue is already finished, send this event instead because
// the primary won't be listening for 'queue:finished' anymore
Cypress.specBridgeCommunicator.toPrimary('uncaught:error', { err })
return
}
const currentAssertionUserInvocationStack = cy.state('current').get('currentAssertionCommand')?.get('userInvocationStack')
const userInvocationStack = cy.state('current').get('userInvocationStack')
cy.stop()
Cypress.specBridgeCommunicator.toPrimary('queue:finished', { err, crossOriginUserInvocationStack: currentAssertionUserInvocationStack || userInvocationStack }, { syncGlobals: true })
})
// the name of this function is used to verify if privileged commands are
// properly called. it shouldn't be removed and if the name is changed, it
// needs to also be changed in server/lib/browsers/privileged-channel.js
function invokeOriginFn (callback) {
return window.eval(`(${callback})`)(args)
}
try {
const callback = await getCallbackFn(fn, file)
const value = invokeOriginFn(callback)
// If we detect a non promise value with commands in queue, throw an error
if (value && cy.queue.length > 0 && !value.then) {
$errUtils.throwErrByPath('origin.callback_mixes_sync_and_async', {
args: { value: $utils.stringify(value) },
})
} else {
const hasCommands = !!cy.queue.length
// If there are queued commands, their yielded value will be preferred
// over the value resolved by a return promise. Don't send the subject
// or the primary will think we're done already with a sync-returned
// value
const subject = hasCommands ? undefined : await value
Cypress.specBridgeCommunicator.toPrimary('ran:origin:fn', {
subject,
finished: !hasCommands,
}, {
// Only sync the globals if there are no commands in queue
// (for instance, only assertions exist in the callback)
// since it means the callback is finished at this point
syncGlobals: !hasCommands,
})
if (!hasCommands) {
queueFinished = true
setRunnableStateToPassed()
clearOriginLogGroupState()
return
}
}
} catch (err) {
setRunnableStateToPassed()
clearOriginLogGroupState()
Cypress.specBridgeCommunicator.toPrimary('ran:origin:fn', { err }, { syncGlobals: true })
return
}
})
}