Skip to content

Commit d80bf84

Browse files
committed
ts
1 parent 32afcfb commit d80bf84

File tree

3 files changed

+161
-153
lines changed

3 files changed

+161
-153
lines changed

packages/driver/src/cypress/command_queue.ts

Lines changed: 149 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type $Command from './command'
1111
import type { StateFunc } from './state'
1212
import type { $Cy } from './cy'
1313
import type { IStability } from '../cy/stability'
14+
import { isJquery } from '../dom/jquery'
1415

1516
const debugErrors = Debug('cypress:driver:errors')
1617

@@ -228,7 +229,7 @@ export class CommandQueue extends Queue<$Command> {
228229
this.state('isStable', true)
229230
}
230231

231-
private runCommand (command: $Command) {
232+
private async runCommand (command: $Command) {
232233
const startTime = performance.now()
233234
const isQuery = command.get('query')
234235
const name = command.get('name')
@@ -244,177 +245,178 @@ export class CommandQueue extends Queue<$Command> {
244245
this.state('current', command)
245246
this.state('chainerId', command.get('chainerId'))
246247

247-
return this.stability.whenStable(() => {
248-
this.state('nestedIndex', this.index)
249-
250-
return command.get('args')
251-
})
252-
.then((args: any) => {
253-
// store this if we enqueue new commands
254-
// to check for promise violations
255-
let ret
256-
let enqueuedCmd
257-
258-
// Queries can invoke other queries - they are synchronous, and get added to the subject chain without
259-
// issue. But they cannot contain commands, which are async.
260-
// This callback watches to ensure users don't try and invoke any commands while inside a query.
261-
const commandEnqueued = (obj: Cypress.EnqueuedCommandAttributes) => {
262-
if (isQuery && !obj.query) {
263-
$errUtils.throwErrByPath(
264-
'query_command.invoked_action', {
265-
args: {
266-
name,
267-
action: obj.name,
268-
},
269-
},
270-
)
271-
}
272-
273-
return enqueuedCmd = obj
274-
}
275-
276-
// only check for command enqueuing when none
277-
// of our args are functions else commands
278-
// like cy.then or cy.each would always fail
279-
// since they return promises and queue more
280-
// new commands
281-
if ($utils.noArgsAreAFunction(args)) {
282-
Cypress.once('command:enqueued', commandEnqueued)
283-
}
284-
285-
args = [command.get('chainerId'), ...args]
286-
287-
// run the command's fn with runnable's context
248+
let args = await new Promise<any>((resolve, reject) => {
288249
try {
289-
command.start()
290-
ret = __stackReplacementMarker(command.get('fn'), args)
291-
292-
// Queries return a function which takes the current subject and returns the next subject. We wrap this in
293-
// retryQuery() - and let it retry until it passes, times out or is cancelled.
294-
// We save the original return value on the $Command though - it's what gets added to the subject chain later.
295-
if (isQuery) {
296-
command.set('queryFn', ret)
297-
ret = retryQuery(command, ret, this.cy)
298-
}
250+
this.stability.whenStable(() => {
251+
this.state('nestedIndex', this.index)
252+
resolve(command.get('args'))
253+
})
299254
} catch (err) {
300-
throw err
301-
} finally {
302-
// always remove this listener
303-
Cypress.removeListener('command:enqueued', commandEnqueued)
255+
reject(err)
304256
}
257+
})
305258

306-
this.state('commandIntermediateValue', ret)
307-
308-
// we cannot pass our cypress instance or our chainer
309-
// back into bluebird else it will create a thenable
310-
// which is never resolved
311-
if (this.cy.isCy(ret)) {
312-
return null
313-
}
259+
// store this if we enqueue new commands
260+
// to check for promise violations
261+
let subject
262+
let enqueuedCmd
314263

315-
if (!(!enqueuedCmd || !$utils.isPromiseLike(ret))) {
264+
// Queries can invoke other queries - they are synchronous, and get added to the subject chain without
265+
// issue. But they cannot contain commands, which are async.
266+
// This callback watches to ensure users don't try and invoke any commands while inside a query.
267+
const commandEnqueued = (obj: Cypress.EnqueuedCommandAttributes) => {
268+
if (isQuery && !obj.query) {
316269
$errUtils.throwErrByPath(
317-
'miscellaneous.command_returned_promise_and_commands', {
270+
'query_command.invoked_action', {
318271
args: {
319-
current: name,
320-
called: enqueuedCmd.name,
272+
name,
273+
action: obj.name,
321274
},
322275
},
323276
)
324277
}
325278

326-
if (!(!enqueuedCmd || !!_.isUndefined(ret))) {
327-
ret = _.isFunction(ret) ?
328-
ret.toString() :
329-
$utils.stringify(ret)
330-
331-
// if we got a return value and we enqueued
332-
// a new command and we didn't return cy
333-
// or an undefined value then throw
334-
$errUtils.throwErrByPath(
335-
'miscellaneous.returned_value_and_commands_from_custom_command', {
336-
args: { current: name, returned: ret },
337-
},
338-
)
339-
}
340-
341-
return ret
342-
})
343-
.then((subject) => {
344-
// we may be given a regular array here so
345-
// we need to re-wrap the array in jquery
346-
// if that's the case if the first item
347-
// in this subject is a jquery element.
348-
// we want to do this because in 3.1.2 there
349-
// was a regression when wrapping an array of elements
350-
const firstSubject = $utils.unwrapFirst(subject)
351-
352-
// if ret is a DOM element and its not an instance of our own jQuery
353-
if (subject && $dom.isElement(firstSubject) && !$utils.isInstanceOf(subject, $)) {
354-
// set it back to our own jquery object
355-
// to prevent it from being passed downstream
356-
// TODO: enable turning this off
357-
// wrapSubjectsInJquery: false
358-
// which will just pass subjects downstream
359-
// without modifying them
360-
subject = $dom.wrap(subject)
361-
}
279+
return enqueuedCmd = obj
280+
}
362281

363-
command.set({ subject })
364-
command.pass()
282+
// only check for command enqueuing when none
283+
// of our args are functions else commands
284+
// like cy.then or cy.each would always fail
285+
// since they return promises and queue more
286+
// new commands
287+
if ($utils.noArgsAreAFunction(args)) {
288+
Cypress.once('command:enqueued', commandEnqueued)
289+
}
365290

366-
const numElements = subject ? subject.length ?? 1 : 0
291+
args = [command.get('chainerId'), ...args]
367292

368-
// end / snapshot our logs if they need it
369-
command.finishLogs()
293+
// run the command's fn with runnable's context
294+
try {
295+
command.start()
296+
subject = __stackReplacementMarker(command.get('fn'), args)
370297

298+
// Queries return a function which takes the current subject and returns the next subject. We wrap this in
299+
// retryQuery() - and let it retry until it passes, times out or is cancelled.
300+
// We save the original return value on the $Command though - it's what gets added to the subject chain later.
371301
if (isQuery) {
372-
subject = command.get('queryFn')
373-
// For queries, the "subject" here is the query's return value, which is a function which
374-
// accepts a subject and returns a subject, and can be re-invoked at any time.
375-
376-
subject.commandName = name
377-
subject.args = command.get('args')
378-
379-
// Even though we've snapshotted, we only end the logs a query's logs if we're at the end of a query
380-
// chain - either there is no next command (end of a test), the next command is an action, or the next
381-
// command belongs to another chainer (end of a chain).
382-
383-
// This is done so that any query's logs remain in the 'pending' state until the subject chain is finished.
384-
this.cy.addQueryToChainer(command.get('chainerId'), subject)
385-
} else {
386-
// For commands, the "subject" here is the command's return value, which replaces
387-
// the current subject chain. We cannot re-invoke commands - the return value here is final.
388-
this.cy.setSubjectForChainer(command.get('chainerId'), [subject])
302+
command.set('queryFn', subject)
303+
subject = retryQuery(command, subject, this.cy)
389304
}
305+
} catch (err) {
306+
throw err
307+
} finally {
308+
// always remove this listener
309+
Cypress.removeListener('command:enqueued', commandEnqueued)
310+
}
390311

391-
// TODO: This line was causing subjects to be cleaned up prematurely in some instances (Specifically seen on the within command)
392-
// The command log would print the yielded value as null if checked outside of the current command chain.
393-
// this.cleanSubjects()
312+
this.state('commandIntermediateValue', subject)
394313

395-
this.state({
396-
commandIntermediateValue: undefined,
397-
// reset the nestedIndex back to null
398-
nestedIndex: null,
399-
// we're finished with the current command so set it back to null
400-
current: null,
401-
})
314+
// we cannot pass our cypress instance or our chainer
315+
// back into bluebird else it will create a thenable
316+
// which is never resolved
317+
if (this.cy.isCy(subject)) {
318+
return null
319+
}
402320

403-
const duration = performance.now() - startTime
321+
if (!(!enqueuedCmd || !$utils.isPromiseLike(subject))) {
322+
$errUtils.throwErrByPath(
323+
'miscellaneous.command_returned_promise_and_commands', {
324+
args: {
325+
current: name,
326+
called: enqueuedCmd.name,
327+
},
328+
},
329+
)
330+
}
404331

405-
Cypress.automation('log:command:performance', {
406-
name: command?.attributes?.name ?? 'unknown',
407-
startTime,
408-
duration,
409-
detail: {
410-
runnableTitle: (this.state('runnable') ?? {}).title ?? 'unknown',
411-
spec: Cypress.spec.relative,
412-
numElements,
332+
if (!(!enqueuedCmd || !!_.isUndefined(subject))) {
333+
subject = _.isFunction(subject) ?
334+
subject.toString() :
335+
$utils.stringify(subject)
336+
337+
// if we got a return value and we enqueued
338+
// a new command and we didn't return cy
339+
// or an undefined value then throw
340+
$errUtils.throwErrByPath(
341+
'miscellaneous.returned_value_and_commands_from_custom_command', {
342+
args: { current: name, returned: subject },
413343
},
414-
})
344+
)
345+
}
346+
347+
// we may be given a regular array here so
348+
// we need to re-wrap the array in jquery
349+
// if that's the case if the first item
350+
// in this subject is a jquery element.
351+
// we want to do this because in 3.1.2 there
352+
// was a regression when wrapping an array of elements
353+
const firstSubject = $utils.unwrapFirst(subject)
354+
355+
// if ret is a DOM element and its not an instance of our own jQuery
356+
if (subject && $dom.isElement(firstSubject) && !$utils.isInstanceOf(subject, $)) {
357+
// set it back to our own jquery object
358+
// to prevent it from being passed downstream
359+
// TODO: enable turning this off
360+
// wrapSubjectsInJquery: false
361+
// which will just pass subjects downstream
362+
// without modifying them
363+
subject = $dom.wrap(subject)
364+
}
365+
366+
command.set({ subject })
367+
command.pass()
368+
369+
const numElements = isJquery(subject) ? subject.length : 0
415370

416-
return subject
371+
// end / snapshot our logs if they need it
372+
command.finishLogs()
373+
374+
if (isQuery) {
375+
subject = command.get('queryFn')
376+
// For queries, the "subject" here is the query's return value, which is a function which
377+
// accepts a subject and returns a subject, and can be re-invoked at any time.
378+
379+
subject.commandName = name
380+
subject.args = command.get('args')
381+
382+
// Even though we've snapshotted, we only end the logs a query's logs if we're at the end of a query
383+
// chain - either there is no next command (end of a test), the next command is an action, or the next
384+
// command belongs to another chainer (end of a chain).
385+
386+
// This is done so that any query's logs remain in the 'pending' state until the subject chain is finished.
387+
this.cy.addQueryToChainer(command.get('chainerId'), subject)
388+
} else {
389+
// For commands, the "subject" here is the command's return value, which replaces
390+
// the current subject chain. We cannot re-invoke commands - the return value here is final.
391+
this.cy.setSubjectForChainer(command.get('chainerId'), [subject])
392+
}
393+
394+
// TODO: This line was causing subjects to be cleaned up prematurely in some instances (Specifically seen on the within command)
395+
// The command log would print the yielded value as null if checked outside of the current command chain.
396+
// this.cleanSubjects()
397+
398+
this.state({
399+
commandIntermediateValue: undefined,
400+
// reset the nestedIndex back to null
401+
nestedIndex: null,
402+
// we're finished with the current command so set it back to null
403+
current: null,
404+
})
405+
406+
const duration = performance.now() - startTime
407+
408+
await Cypress.automation('log:command:performance', {
409+
name: command?.attributes?.name ?? 'unknown',
410+
startTime,
411+
duration,
412+
detail: {
413+
runnableTitle: (this.state('runnable') ?? {}).title ?? 'unknown',
414+
spec: Cypress.spec.relative,
415+
numElements,
416+
},
417417
})
418+
419+
return subject
418420
}
419421

420422
// TypeScript doesn't allow overriding functions with different type signatures

packages/server/lib/automation/commands/record_performance_entry.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function initializePerformanceLogFile () {
5555
}
5656
}
5757

58-
export async function recordPerformanceEntry (entry: CommandPerformanceEntry) {
58+
export function recordPerformanceEntry (entry: CommandPerformanceEntry) {
5959
debug('recording performance entry %o', entry)
6060

6161
if (!performanceLogsEnabled()) {
@@ -82,9 +82,13 @@ export async function recordPerformanceEntry (entry: CommandPerformanceEntry) {
8282
spec,
8383
].map(escapeCsvValue).join(',')
8484

85-
await fs.writeFile(
86-
path.join(logFilePath(), 'performance.log'),
87-
`${row}\n`,
88-
{ flag: 'a' },
89-
)
85+
try {
86+
fsSync.writeFile(
87+
path.join(logFilePath(), 'performance.log'),
88+
`${row}\n`,
89+
{ flag: 'a' },
90+
)
91+
} catch (error) {
92+
debug('error recording performance entry: %s', error)
93+
}
9094
}

packages/server/lib/browsers/bidi_automation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { bidiGetUrl } from '../automation/commands/get_url'
2525
import { bidiReloadFrame } from '../automation/commands/reload_frame'
2626
import { bidiNavigateHistory } from '../automation/commands/navigate_history'
2727
import { bidiGetFrameTitle } from '../automation/commands/get_frame_title'
28+
import { recordPerformanceEntry } from '../automation/commands/record_performance_entry'
29+
2830
import type { StorageCookieFilter, StoragePartialCookie as BidiStoragePartialCookie } from 'webdriver/build/bidi/remoteTypes'
2931

3032
const BIDI_DEBUG_NAMESPACE = 'cypress:server:browsers:bidi_automation'

0 commit comments

Comments
 (0)