Repository: Codeception/CodeceptJS (js)

Issue:
[FileSystem helper] How to wait for file exists using FileSystem helper?
Is there a simple way to wait for file exists?

How about making I.seeFile() waiting by default the same way as I.see() does?

Pull request:
improve file system helper
## Motivation/Description of the PR

Applicable helpers:

- [ ] Webdriver
- [ ] Puppeteer
- [ ] Nightmare
- [ ] REST
- [ ] Appium
- [ ] Protractor
- [ ] TestCafe
- [x] Filesystem

- add waitForFile Resolves #1587
- add seeFileContentsEqualReferenceFile
- fix encoding

## Type of change

- [ ] Breaking changes
- [x] New functionality
- [x] Bug fix
- [x] Documentation changes/updates
- [ ] Hot fix
- [ ] Markdown files fix - not related to source code

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] Tests have been added
- [x] Documentation has been added (Run `npm run docs`)
- [x] Lint checking (Run `npm run lint`)
- [x] Local tests are passed (Run `npm test`)


Patch:
diff --git a/docs/helpers/FileSystem.md b/docs/helpers/FileSystem.md
index 5a10454c..22124afa 100644
--- a/docs/helpers/FileSystem.md
+++ b/docs/helpers/FileSystem.md
@@ -39,7 +39,7 @@ Checks that contents of file found by `seeFile` doesn't equal to text.
 #### Parameters
 
 -   `text` **[string][1]** 
--   `encoding` **[string][1]** 
+-   `encoding` **[string][1]**  
 
 ### dontSeeInThisFile
 
@@ -48,7 +48,7 @@ Checks that file found by `seeFile` doesn't include text.
 #### Parameters
 
 -   `text` **[string][1]** 
--   `encoding` **[string][1]** 
+-   `encoding` **[string][1]**  
 
 ### grabFileNames
 
@@ -76,7 +76,17 @@ Checks that contents of file found by `seeFile` equal to text.
 #### Parameters
 
 -   `text` **[string][1]** 
--   `encoding` **[string][1]** 
+-   `encoding` **[string][1]**  
+
+### seeFileContentsEqualReferenceFile
+
+Checks that contents of the file found by `seeFile` equal to contents of the file at `pathToReferenceFile`.
+
+#### Parameters
+
+-   `pathToReferenceFile` **[string][1]** 
+-   `encoding` **[string][1]**  
+-   `encodingReference` **[string][1]**  
 
 ### seeFileNameMatching
 
@@ -100,7 +110,23 @@ Checks that file found by `seeFile` includes a text.
 #### Parameters
 
 -   `text` **[string][1]** 
--   `encoding` **[string][1]** 
+-   `encoding` **[string][1]**  
+
+### waitForFile
+
+Waits for file to be present in current directory.
+
+```js
+I.handleDownloads();
+I.click('Download large File');
+I.amInPath('output/downloads');
+I.waitForFile('largeFilesName.txt', 10); // wait 10 seconds for file
+```
+
+#### Parameters
+
+-   `name` **[string][1]** 
+-   `sec` **[number][2]** seconds to wait 
 
 ### writeToFile
 
@@ -111,4 +137,26 @@ Writes test to file
 -   `name` **[string][1]** 
 -   `text` **[string][1]** 
 
+## fileExistsOrIsCreated
+
+### Parameters
+
+-   `file` **[string][1]** 
+-   `timeout` **[number][2]** 
+
+Returns **[Promise][3]** 
+
+## getFileContents
+
+### Parameters
+
+-   `file` **[string][1]** 
+-   `encoding` **[string][1]**  
+
+Returns **[string][1]** 
+
 [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
+
+[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
+
+[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
diff --git a/lib/helper/FileSystem.js b/lib/helper/FileSystem.js
index 5534f598..3d8e5517 100644
--- a/lib/helper/FileSystem.js
+++ b/lib/helper/FileSystem.js
@@ -60,6 +60,28 @@ class FileSystem extends Helper {
     assert.ok(fileExists(this.file), `File ${name} not found in ${this.dir}`);
   }
 
+  /**
+   * Waits for file to be present in current directory.
+   *
+   * ```js
+   * I.handleDownloads();
+   * I.click('Download large File');
+   * I.amInPath('output/downloads');
+   * I.waitForFile('largeFilesName.txt', 10); // wait 10 seconds for file
+   * ```
+   * @param {string} name
+   * @param {number} [sec=1] seconds to wait
+   */
+  async waitForFile(name, sec = 1) {
+    if (sec === 0) assert.fail('Use `seeFile` instead of waiting 0 seconds!');
+    const waitTimeout = sec * 1000;
+    this.file = path.join(this.dir, name);
+    this.debugSection('File', this.file);
+    return isFileExists(this.file, waitTimeout).catch((err) => {
+      throw new Error(`file (${name}) still not present in directory ${this.dir} after ${waitTimeout / 1000} sec`);
+    });
+  }
+
   /**
   * Checks that file with a name including given text exists in the current directory.
   *
@@ -80,9 +102,9 @@ class FileSystem extends Helper {
   /**
    * Checks that file found by `seeFile` includes a text.
    * @param {string} text
-   * @param {string} encoding
+   * @param {string} [encoding='utf8']
    */
-  seeInThisFile(text, encoding) {
+  seeInThisFile(text, encoding = 'utf8') {
     const content = getFileContents(this.file, encoding);
     fileIncludes(this.file).assert(text, content);
   }
@@ -90,9 +112,9 @@ class FileSystem extends Helper {
   /**
    * Checks that file found by `seeFile` doesn't include text.
    * @param {string} text
-   * @param {string} encoding
+   * @param {string} [encoding='utf8']
    */
-  dontSeeInThisFile(text, encoding) {
+  dontSeeInThisFile(text, encoding = 'utf8') {
     const content = getFileContents(this.file, encoding);
     fileIncludes(this.file).negate(text, content);
   }
@@ -100,19 +122,33 @@ class FileSystem extends Helper {
   /**
    * Checks that contents of file found by `seeFile` equal to text.
    * @param {string} text
-   * @param {string} encoding
+   * @param {string} [encoding='utf8']
    */
-  seeFileContentsEqual(text, encoding) {
+  seeFileContentsEqual(text, encoding = 'utf8') {
     const content = getFileContents(this.file, encoding);
     fileEquals(this.file).assert(text, content);
   }
 
+  /**
+   * Checks that contents of the file found by `seeFile` equal to contents of the file at `pathToReferenceFile`.
+   * @param {string} pathToReferenceFile
+   * @param {string} [encoding='utf8']
+   * @param {string} [encodingReference='utf8']
+   */
+  seeFileContentsEqualReferenceFile(pathToReferenceFile, encoding = 'utf8', encodingReference = '') {
+    const content = getFileContents(this.file, encoding);
+    assert.ok(fileExists(pathToReferenceFile), `Reference file ${pathToReferenceFile} not found.`);
+    encodingReference = encodingReference || encoding;
+    const expectedContent = getFileContents(pathToReferenceFile, encodingReference);
+    fileEquals(this.file).assert(expectedContent, content);
+  }
+
   /**
    * Checks that contents of file found by `seeFile` doesn't equal to text.
    * @param {string} text
-   * @param {string} encoding
+   * @param {string} [encoding='utf8']
    */
-  dontSeeFileContentsEqual(text, encoding) {
+  dontSeeFileContentsEqual(text, encoding = 'utf8') {
     const content = getFileContents(this.file, encoding);
     fileEquals(this.file).negate(text, content);
   }
@@ -133,10 +169,47 @@ class FileSystem extends Helper {
   }
 }
 
-function getFileContents(file, encoding) {
+module.exports = FileSystem;
+
+/**
+ * @param {string} file
+ * @param {string} [encoding='utf8']
+ * @returns {string}
+ */
+function getFileContents(file, encoding = 'utf8') {
   if (!file) assert.fail('No files were opened, please use seeFile action');
-  encoding = encoding || 'utf8';
-  return fs.readFileSync(file, 'utf8');
+  if (encoding === '') assert.fail('Encoding is an empty string, please set a valid encoding');
+  return fs.readFileSync(file, encoding);
 }
 
-module.exports = FileSystem;
+/**
+ * @param {string} file
+ * @param {number} timeout
+ * @returns {Promise}
+ */
+function isFileExists(file, timeout) {
+  return new Promise(((resolve, reject) => {
+    const timer = setTimeout(() => {
+      watcher.close();
+      reject(new Error('File did not exists and was not created during the timeout.'));
+    }, timeout);
+
+    const dir = path.dirname(file);
+    const basename = path.basename(file);
+    const watcher = fs.watch(dir, (eventType, filename) => {
+      if (eventType === 'rename' && filename === basename) {
+        clearTimeout(timer);
+        watcher.close();
+        resolve();
+      }
+    });
+
+    fs.access(file, fs.constants.R_OK, (err) => {
+      if (!err) {
+        clearTimeout(timer);
+        watcher.close();
+        resolve();
+      }
+    });
+  }));
+}
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 697c1276..4f2762ae 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -1782,7 +1782,7 @@ class WebDriver extends Helper {
     assertElementExists(res, locator);
 
     return res.waitForClickable({
-      timeout: waitTimeout,
+      timeout: waitTimeout * 1000,
       timeoutMsg: `element ${res.selector} still not clickable after ${waitTimeout} sec`,
     });
   }


Test patch:
diff --git a/test/data/sandbox/configs/bootstrap/failed_test.js b/test/data/sandbox/configs/bootstrap/failed_test.js
index fc45269f..e42c260f 100644
--- a/test/data/sandbox/configs/bootstrap/failed_test.js
+++ b/test/data/sandbox/configs/bootstrap/failed_test.js
@@ -5,3 +5,9 @@ Scenario('check current dir', (I) => {
   I.say('hello world');
   I.seeFile('unknown.js');
 }).tag('slow').tag('@important');
+
+Scenario('check current dir with wait', (I) => {
+  I.amInPath('.');
+  I.say('hello world');
+  I.waitForFile('unknown.js', 1);
+}).tag('slow').tag('@important');
diff --git a/test/data/sandbox/configs/bootstrap/fs_test.js b/test/data/sandbox/configs/bootstrap/fs_test.js
index ac8b3dc3..6b716c24 100644
--- a/test/data/sandbox/configs/bootstrap/fs_test.js
+++ b/test/data/sandbox/configs/bootstrap/fs_test.js
@@ -1,7 +1,14 @@
 Feature('Filesystem').tag('main');
 
-Scenario('check current dir', (I) => {
+Scenario('see content in file', (I) => {
   I.amInPath('.');
   I.say('hello world');
   I.seeFile('fs_test.js');
+  I.seeFileContentsEqualReferenceFile(__filename);
+}).tag('slow').tag('@important');
+
+Scenario('wait for file in current dir', (I) => {
+  I.amInPath('.');
+  I.say('hello world');
+  I.waitForFile('fs_test.js');
 }).tag('slow').tag('@important');
diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js
index 1d677302..fc150d1d 100644
--- a/test/helper/Puppeteer_test.js
+++ b/test/helper/Puppeteer_test.js
@@ -832,8 +832,7 @@ describe('Puppeteer', function () {
       await I.amOnPage('/form/download');
       await I.handleDownloads();
       await I.click('Download file');
-      await I.wait(5);
-      await FS.seeFile('downloads/avatar.jpg');
+      await FS.waitForFile('downloads/avatar.jpg', 5);
     });
   });