@@ -4,118 +4,132 @@ import { basename } from 'path'
44import $errUtils from '../../cypress/error_utils'
55import type { Log } from '../../cypress/log'
66
7- interface InternalReadFileOptions extends Partial < Cypress . Loggable & Cypress . Timeoutable > {
8- _log ?: Log
9- encoding : Cypress . Encodings
7+ interface ReadFileOptions extends Partial < Cypress . Loggable & Cypress . Timeoutable > {
8+ encoding ?: Cypress . Encodings
109}
1110
1211interface InternalWriteFileOptions extends Partial < Cypress . WriteFileOptions & Cypress . Timeoutable > {
1312 _log ?: Log
1413}
1514
1615export default ( Commands , Cypress , cy , state ) => {
17- Commands . addAll ( {
18- readFile ( file , encoding , userOptions : Partial < Cypress . Loggable & Cypress . Timeoutable > = { } ) {
19- if ( _ . isObject ( encoding ) ) {
20- userOptions = encoding
21- encoding = undefined
16+ Commands . addQuery ( 'readFile' , function readFile ( file , encoding , options : ReadFileOptions = { } ) {
17+ if ( _ . isObject ( encoding ) ) {
18+ options = encoding
19+ encoding = options . encoding
20+ }
21+
22+ encoding = encoding === undefined ? 'utf8' : encoding
23+
24+ const timeout = options . timeout ?? Cypress . config ( 'defaultCommandTimeout' )
25+
26+ this . set ( 'timeout' , timeout )
27+ this . set ( 'ensureExistenceFor' , 'subject' )
28+
29+ const log = options . log !== false && Cypress . log ( { message : file , timeout } )
30+
31+ if ( ! file || ! _ . isString ( file ) ) {
32+ $errUtils . throwErrByPath ( 'files.invalid_argument' , {
33+ args : { cmd : 'readFile' , file } ,
34+ } )
35+ }
36+
37+ let fileResult : any = null
38+ let filePromise : Promise < void > | null = null
39+ let mostRecentError = $errUtils . cypressErrByPath ( 'files.read_timed_out' , {
40+ args : { file } ,
41+ } )
42+
43+ const createFilePromise = ( ) => {
44+ // If we already have a pending request to the backend, we'll wait
45+ // for that one to resolve instead of creating a new one.
46+ if ( filePromise ) {
47+ return
2248 }
2349
24- const options : InternalReadFileOptions = _ . defaults ( { } , userOptions , {
50+ fileResult = null
51+ filePromise = Cypress . backend ( 'read:file' , file , { encoding } )
52+ . timeout ( timeout )
53+ . then ( ( result ) => {
2554 // https://github.com/cypress-io/cypress/issues/1558
26- // If no encoding is specified, then Cypress has historically defaulted
27- // to `utf8`, because of it's focus on text files. This is in contrast to
28- // NodeJs, which defaults to binary. We allow users to pass in `null`
29- // to restore the default node behavior.
30- encoding : encoding === undefined ? 'utf8' : encoding ,
31- log : true ,
32- timeout : Cypress . config ( 'defaultCommandTimeout' ) ,
55+ // https://github.com/cypress-io/cypress/issues/20683
56+ // We invoke Buffer.from() in order to transform this from an ArrayBuffer -
57+ // which socket.io uses to transfer the file over the websocket - into a `Buffer`.
58+ if ( encoding === null && result . contents !== null ) {
59+ result . contents = Buffer . from ( result . contents )
60+ }
61+
62+ // Add the filename to the current command, in case we need it later (such as when storing an alias)
63+ state ( 'current' ) . set ( 'fileName' , basename ( result . filePath ) )
64+
65+ fileResult = result
3366 } )
67+ . catch ( ( err ) => {
68+ if ( err . name === 'TimeoutError' ) {
69+ $errUtils . throwErrByPath ( 'files.timed_out' , {
70+ args : { cmd : 'readFile' , file, timeout } ,
71+ retry : false ,
72+ } )
73+ }
3474
35- const consoleProps = { }
75+ // Non-ENOENT errors are not retried
76+ if ( err . code !== 'ENOENT' ) {
77+ $errUtils . throwErrByPath ( 'files.unexpected_error' , {
78+ args : { cmd : 'readFile' , action : 'read' , file, filePath : err . filePath , error : err . message } ,
79+ errProps : { retry : false } ,
80+ } )
81+ }
3682
37- if ( options . log ) {
38- options . _log = Cypress . log ( {
39- message : file ,
40- timeout : options . timeout ,
41- consoleProps ( ) {
42- return consoleProps
43- } ,
83+ // We have a ENOENT error - the file doesn't exist. Whether this is an error or not is deterimened
84+ // by verifyUpcomingAssertions, when the command_queue receives the null file contents.
85+ fileResult = { contents : null , filePath : err . filePath }
86+ } )
87+ . catch ( ( err ) => mostRecentError = err )
88+ // Pass or fail, we always clear the filePromise, so future retries know there's no
89+ // live request to the server.
90+ . finally ( ( ) => filePromise = null )
91+ }
92+
93+ // When an assertion attached to this command fails, then we want to throw away the existing result
94+ // and create a new promise to read a new one.
95+ this . set ( 'onFail' , ( err , timedOut ) => {
96+ if ( err . type === 'existence' ) {
97+ // file exists but it shouldn't - or - file doesn't exist but it should
98+ const errPath = fileResult . contents ? 'files.existent' : 'files.nonexistent'
99+ const { message, docsUrl } = $errUtils . cypressErrByPath ( errPath , {
100+ args : { cmd : 'readFile' , file, filePath : fileResult . filePath } ,
44101 } )
45- }
46102
47- if ( ! file || ! _ . isString ( file ) ) {
48- $errUtils . throwErrByPath ( 'files.invalid_argument' , {
49- onFail : options . _log ,
50- args : { cmd : 'readFile' , file } ,
51- } )
103+ err . message = message
104+ err . docsUrl = docsUrl
52105 }
53106
54- // We clear the default timeout so we can handle
55- // the timeout ourselves
56- cy . clearTimeout ( )
107+ createFilePromise ( )
108+ } )
57109
58- const verifyAssertions = ( ) => {
59- return Cypress . backend ( 'read:file' , file , _ . pick ( options , 'encoding' ) ) . timeout ( options . timeout )
60- . catch ( ( err ) => {
61- if ( err . name === 'TimeoutError' ) {
62- return $errUtils . throwErrByPath ( 'files.timed_out' , {
63- onFail : options . _log ,
64- args : { cmd : 'readFile' , file, timeout : options . timeout } ,
65- } )
66- }
67-
68- // Non-ENOENT errors are not retried
69- if ( err . code !== 'ENOENT' ) {
70- return $errUtils . throwErrByPath ( 'files.unexpected_error' , {
71- onFail : options . _log ,
72- args : { cmd : 'readFile' , action : 'read' , file, filePath : err . filePath , error : err . message } ,
73- } )
74- }
75-
76- return {
77- contents : null ,
78- filePath : err . filePath ,
79- }
80- } ) . then ( ( { filePath, contents } ) => {
81- // https://github.com/cypress-io/cypress/issues/1558
82- // https://github.com/cypress-io/cypress/issues/20683
83- // We invoke Buffer.from() in order to transform this from an ArrayBuffer -
84- // which socket.io uses to transfer the file over the websocket - into a `Buffer`.
85- if ( options . encoding === null && contents !== null ) {
86- contents = Buffer . from ( contents )
87- }
88-
89- // Add the filename as a symbol, in case we need it later (such as when storing an alias)
90- state ( 'current' ) . set ( 'fileName' , basename ( filePath ) )
91-
92- consoleProps [ 'File Path' ] = filePath
93- consoleProps [ 'Contents' ] = contents
94-
95- return cy . verifyUpcomingAssertions ( contents , options , {
96- ensureExistenceFor : 'subject' ,
97- onFail ( err ) {
98- if ( err . type !== 'existence' ) {
99- return
100- }
101-
102- // file exists but it shouldn't - or - file doesn't exist but it should
103- const errPath = contents ? 'files.existent' : 'files.nonexistent'
104- const { message, docsUrl } = $errUtils . cypressErrByPath ( errPath , {
105- args : { cmd : 'readFile' , file, filePath } ,
106- } )
107-
108- err . message = message
109- err . docsUrl = docsUrl
110- } ,
111- onRetry : verifyAssertions ,
112- } )
113- } )
110+ return ( ) => {
111+ // Once we've read a file, that remains the result, unless it's cleared
112+ // because of a failed assertion in `onFail` above.
113+ if ( fileResult ) {
114+ const props = {
115+ 'Contents' : fileResult ?. contents ,
116+ 'File Path' : fileResult ?. filePath ,
117+ }
118+
119+ log && state ( 'current' ) === this && log . set ( 'consoleProps' , ( ) => props )
120+
121+ return fileResult . contents
114122 }
115123
116- return verifyAssertions ( )
117- } ,
124+ createFilePromise ( )
118125
126+ // If we don't have a result, then the promise is pending.
127+ // Throw an error and wait for the promise to eventually resolve on a future retry.
128+ throw mostRecentError
129+ }
130+ } )
131+
132+ Commands . addAll ( {
119133 writeFile ( fileName , contents , encoding , userOptions : Partial < Cypress . WriteFileOptions & Cypress . Timeoutable > = { } ) {
120134 if ( _ . isObject ( encoding ) ) {
121135 userOptions = encoding
0 commit comments