Repository: webpack-contrib/copy-webpack-plugin
Pull request #478 (2020-05-15 16:37:36)

Test coverage
<!--
  HOLY CRAP a Pull Request. We ❤️ those!

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  Please place an x (no spaces!) in all [ ] that apply
-->

This PR contains a:

- [ ] **bugfix**
- [ ] new **feature**
- [x] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

refactor code

### Breaking Changes

No, because we don't respect cwd before

### Additional Info

No

codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=h1) Report
> Merging [#478](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/commit/29254e394cb695d89b477f44f3a3bf8c99c74ca7&el=desc) will **increase** coverage by `0.49%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478/graphs/tree.svg?width=650&height=150&src=pr&token=fEoU9iBFGL)](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #478      +/-   ##
==========================================
+ Coverage   96.17%   96.66%   +0.49%     
==========================================
  Files           8        8              
  Lines         209      210       +1     
  Branches       58       57       -1     
==========================================
+ Hits          201      203       +2     
+ Misses          8        7       -1     
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/utils/createPatternGlob.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL2NyZWF0ZVBhdHRlcm5HbG9iLmpz) | `100.00% <ø> (ø)` | |
| [src/postProcessPattern.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478/diff?src=pr&el=tree#diff-c3JjL3Bvc3RQcm9jZXNzUGF0dGVybi5qcw==) | `90.62% <100.00%> (+1.73%)` | :arrow_up: |
| [src/processPattern.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478/diff?src=pr&el=tree#diff-c3JjL3Byb2Nlc3NQYXR0ZXJuLmpz) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=footer). Last update [29254e3...2fcf8a0](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/478?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/options.json b/src/options.json
index 1b19cd9..29ae6b2 100644
--- a/src/options.json
+++ b/src/options.json
@@ -100,5 +100,6 @@
         }
       }
     }
-  }
+  },
+  "required": ["patterns"]
 }
diff --git a/src/postProcessPattern.js b/src/postProcessPattern.js
index 199da4c..1613941 100644
--- a/src/postProcessPattern.js
+++ b/src/postProcessPattern.js
@@ -22,7 +22,7 @@ export default async function postProcessPattern(globalRef, pattern, file) {
   logger.debug(`getting stats for '${file.absoluteFrom}' to write to assets`);
 
   const getStats = pattern.stats
-    ? Promise.resolve().then(() => pattern.stats)
+    ? pattern.stats
     : stat(inputFileSystem, file.absoluteFrom);
 
   let stats;
@@ -31,6 +31,7 @@ export default async function postProcessPattern(globalRef, pattern, file) {
     stats = await getStats;
   } catch (error) {
     compilation.errors.push(error);
+
     return;
   }
 
@@ -38,6 +39,8 @@ export default async function postProcessPattern(globalRef, pattern, file) {
     logger.debug(
       `skipping '${file.absoluteFrom}' because it is empty directory`
     );
+
+    return;
   }
 
   // If this came from a glob, add it to the file watchlist
diff --git a/src/processPattern.js b/src/processPattern.js
index 070ef75..6fc5306 100644
--- a/src/processPattern.js
+++ b/src/processPattern.js
@@ -31,7 +31,15 @@ export default async function processPattern(globalRef, pattern) {
     return Promise.resolve();
   }
 
-  return paths.map((from) => {
+  return paths.map((filepath) => {
+    let from;
+
+    if (typeof filepath === 'string') {
+      from = filepath;
+    } else {
+      from = filepath.path;
+    }
+
     const file = { absoluteFrom: path.resolve(pattern.context, from) };
 
     file.relativeFrom = path.relative(pattern.context, file.absoluteFrom);
@@ -43,13 +51,10 @@ export default async function processPattern(globalRef, pattern) {
     logger.debug(`found ${from}`);
 
     // Change the to path to be relative for webpack
-    if (pattern.toType === 'dir') {
-      file.webpackTo = path.join(pattern.to, file.relativeFrom);
-    } else if (pattern.toType === 'file') {
-      file.webpackTo = pattern.to || file.relativeFrom;
-    } else if (pattern.toType === 'template') {
-      file.webpackTo = pattern.to;
-    }
+    file.webpackTo =
+      pattern.toType === 'dir'
+        ? path.join(pattern.to, file.relativeFrom)
+        : pattern.to;
 
     if (path.isAbsolute(file.webpackTo)) {
       file.webpackTo = path.relative(output, file.webpackTo);
diff --git a/src/utils/createPatternGlob.js b/src/utils/createPatternGlob.js
index 1fe84a0..1c8c5b0 100644
--- a/src/utils/createPatternGlob.js
+++ b/src/utils/createPatternGlob.js
@@ -18,11 +18,9 @@ function createPatternGlob(pattern, globalRef) {
 
   // eslint-disable-next-line no-param-reassign
   pattern.globOptions = {
-    ...{
-      cwd: pattern.context,
-      followSymbolicLinks: true,
-    },
+    ...{ followSymbolicLinks: true },
     ...(pattern.globOptions || {}),
+    ...{ cwd: pattern.context },
   };
 
   switch (pattern.fromType) {
@@ -44,7 +42,6 @@ function createPatternGlob(pattern, globalRef) {
         pattern.globOptions.dot = true;
       }
       /* eslint-enable no-param-reassign */
-
       break;
     case 'file':
       logger.debug(`determined '${pattern.absoluteFrom}' is a file`);
@@ -60,7 +57,6 @@ function createPatternGlob(pattern, globalRef) {
         pattern.globOptions.dot = true;
       }
       /* eslint-enable no-param-reassign */
-
       break;
     default: {
       logger.debug(`determined '${pattern.absoluteFrom}' is a glob`);


Test patch:
diff --git a/test/CopyPlugin.test.js b/test/CopyPlugin.test.js
index 107ae61..e2bed24 100644
--- a/test/CopyPlugin.test.js
+++ b/test/CopyPlugin.test.js
@@ -488,7 +488,7 @@ describe('CopyPlugin', () => {
   });
 
   describe('logging', () => {
-    it('should logging', (done) => {
+    it('should logging when "from" is a file', (done) => {
       const expectedAssetKeys = ['file.txt'];
 
       run({
@@ -504,12 +504,81 @@ describe('CopyPlugin', () => {
             .get('copy-webpack-plugin')
             .map((entry) =>
               entry.args[0].replace(/\\/g, '/').split(root).join('.')
-            );
+            )
+            .sort();
 
           expect(
             Array.from(Object.keys(readAssets(compiler, stats))).sort()
           ).toEqual(expectedAssetKeys);
-          expect({ result: logs }).toMatchSnapshot({ result: logs });
+          expect({ logs }).toMatchSnapshot('logs');
+        })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should logging when "from" is a directory', (done) => {
+      const expectedAssetKeys = [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ];
+
+      run({
+        patterns: [
+          {
+            from: 'directory',
+          },
+        ],
+      })
+        .then(({ compiler, stats }) => {
+          const root = path.resolve(__dirname).replace(/\\/g, '/');
+          const logs = stats.compilation.logging
+            .get('copy-webpack-plugin')
+            .map((entry) =>
+              entry.args[0].replace(/\\/g, '/').split(root).join('.')
+            )
+            .sort();
+
+          expect(
+            Array.from(Object.keys(readAssets(compiler, stats))).sort()
+          ).toEqual(expectedAssetKeys);
+          expect({ logs }).toMatchSnapshot('logs');
+        })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should logging when "from" is a glob', (done) => {
+      const expectedAssetKeys = [
+        'directory/directoryfile.txt',
+        'directory/nested/deep-nested/deepnested.txt',
+        'directory/nested/nestedfile.txt',
+      ];
+
+      run({
+        patterns: [
+          {
+            from: 'directory/**',
+            globOptions: {
+              onlyFiles: false,
+            },
+          },
+        ],
+      })
+        .then(({ compiler, stats }) => {
+          const root = path.resolve(__dirname).replace(/\\/g, '/');
+          const logs = stats.compilation.logging
+            .get('copy-webpack-plugin')
+            .map((entry) =>
+              entry.args[0].replace(/\\/g, '/').split(root).join('.')
+            )
+            .sort();
+
+          expect(
+            Array.from(Object.keys(readAssets(compiler, stats))).sort()
+          ).toEqual(expectedAssetKeys);
+          expect({ logs }).toMatchSnapshot('logs');
         })
         .then(done)
         .catch(done);
diff --git a/test/__snapshots__/CopyPlugin.test.js.snap b/test/__snapshots__/CopyPlugin.test.js.snap
index 5092e9b..f5ab4ad 100644
--- a/test/__snapshots__/CopyPlugin.test.js.snap
+++ b/test/__snapshots__/CopyPlugin.test.js.snap
@@ -1,20 +1,94 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`CopyPlugin logging should logging 1`] = `
+exports[`CopyPlugin logging should logging when "from" is a directory: logs 1`] = `
 Object {
-  "result": Array [
+  "logs": Array [
+    "add ./fixtures/directory as contextDependencies",
+    "begin globbing './fixtures/directory/**/*' with a context of './fixtures/directory'",
+    "determined './fixtures/directory' is a directory",
+    "determined that './fixtures/directory/.dottedfile' should write to '.dottedfile'",
+    "determined that './fixtures/directory/directoryfile.txt' should write to 'directoryfile.txt'",
+    "determined that './fixtures/directory/nested/deep-nested/deepnested.txt' should write to 'nested/deep-nested/deepnested.txt'",
+    "determined that './fixtures/directory/nested/nestedfile.txt' should write to 'nested/nestedfile.txt'",
+    "end to adding additional assets",
+    "found ./fixtures/directory/.dottedfile",
+    "found ./fixtures/directory/directoryfile.txt",
+    "found ./fixtures/directory/nested/deep-nested/deepnested.txt",
+    "found ./fixtures/directory/nested/nestedfile.txt",
+    "getting stats for './fixtures/directory' to determinate 'fromType'",
+    "getting stats for './fixtures/directory/.dottedfile' to write to assets",
+    "getting stats for './fixtures/directory/directoryfile.txt' to write to assets",
+    "getting stats for './fixtures/directory/nested/deep-nested/deepnested.txt' to write to assets",
+    "getting stats for './fixtures/directory/nested/nestedfile.txt' to write to assets",
+    "processing from: 'directory' to: '.'",
+    "reading './fixtures/directory/.dottedfile' to write to assets",
+    "reading './fixtures/directory/directoryfile.txt' to write to assets",
+    "reading './fixtures/directory/nested/deep-nested/deepnested.txt' to write to assets",
+    "reading './fixtures/directory/nested/nestedfile.txt' to write to assets",
     "start to adding additional assets",
-    "processing from: 'file.txt' to: '.'",
-    "getting stats for './fixtures/file.txt' to determinate 'fromType'",
-    "determined './fixtures/file.txt' is a file",
+    "writing '.dottedfile' to compilation assets from './fixtures/directory/.dottedfile'",
+    "writing 'directoryfile.txt' to compilation assets from './fixtures/directory/directoryfile.txt'",
+    "writing 'nested/deep-nested/deepnested.txt' to compilation assets from './fixtures/directory/nested/deep-nested/deepnested.txt'",
+    "writing 'nested/nestedfile.txt' to compilation assets from './fixtures/directory/nested/nestedfile.txt'",
+  ],
+}
+`;
+
+exports[`CopyPlugin logging should logging when "from" is a file: logs 1`] = `
+Object {
+  "logs": Array [
     "add ./fixtures/file.txt as fileDependencies",
     "begin globbing './fixtures/file.txt' with a context of './fixtures'",
-    "found ./fixtures/file.txt",
+    "determined './fixtures/file.txt' is a file",
     "determined that './fixtures/file.txt' should write to 'file.txt'",
+    "end to adding additional assets",
+    "found ./fixtures/file.txt",
+    "getting stats for './fixtures/file.txt' to determinate 'fromType'",
     "getting stats for './fixtures/file.txt' to write to assets",
+    "processing from: 'file.txt' to: '.'",
     "reading './fixtures/file.txt' to write to assets",
+    "start to adding additional assets",
     "writing 'file.txt' to compilation assets from './fixtures/file.txt'",
+  ],
+}
+`;
+
+exports[`CopyPlugin logging should logging when "from" is a glob: logs 1`] = `
+Object {
+  "logs": Array [
+    "add ./fixtures/directory as contextDependencies",
+    "add ./fixtures/directory/directoryfile.txt as fileDependencies",
+    "add ./fixtures/directory/nested/deep-nested/deepnested.txt as fileDependencies",
+    "add ./fixtures/directory/nested/nestedfile.txt as fileDependencies",
+    "begin globbing './fixtures/directory/**' with a context of './fixtures'",
+    "determined './fixtures/directory/**' is a glob",
+    "determined that './fixtures/directory/directoryfile.txt' should write to 'directory/directoryfile.txt'",
+    "determined that './fixtures/directory/nested' should write to 'directory/nested'",
+    "determined that './fixtures/directory/nested/deep-nested' should write to 'directory/nested/deep-nested'",
+    "determined that './fixtures/directory/nested/deep-nested/deepnested.txt' should write to 'directory/nested/deep-nested/deepnested.txt'",
+    "determined that './fixtures/directory/nested/nestedfile.txt' should write to 'directory/nested/nestedfile.txt'",
     "end to adding additional assets",
+    "found ./fixtures/directory/directoryfile.txt",
+    "found ./fixtures/directory/nested",
+    "found ./fixtures/directory/nested/deep-nested",
+    "found ./fixtures/directory/nested/deep-nested/deepnested.txt",
+    "found ./fixtures/directory/nested/nestedfile.txt",
+    "getting stats for './fixtures/directory/**' to determinate 'fromType'",
+    "getting stats for './fixtures/directory/directoryfile.txt' to write to assets",
+    "getting stats for './fixtures/directory/nested' to write to assets",
+    "getting stats for './fixtures/directory/nested/deep-nested' to write to assets",
+    "getting stats for './fixtures/directory/nested/deep-nested/deepnested.txt' to write to assets",
+    "getting stats for './fixtures/directory/nested/nestedfile.txt' to write to assets",
+    "processing from: 'directory/**' to: '.'",
+    "reading './fixtures/directory/directoryfile.txt' to write to assets",
+    "reading './fixtures/directory/nested/deep-nested/deepnested.txt' to write to assets",
+    "reading './fixtures/directory/nested/nestedfile.txt' to write to assets",
+    "skipping './fixtures/directory/nested' because it is empty directory",
+    "skipping './fixtures/directory/nested/deep-nested' because it is empty directory",
+    "start to adding additional assets",
+    "writing 'directory/directoryfile.txt' to compilation assets from './fixtures/directory/directoryfile.txt'",
+    "writing 'directory/nested/deep-nested/deepnested.txt' to compilation assets from './fixtures/directory/nested/deep-nested/deepnested.txt'",
+    "writing 'directory/nested/nestedfile.txt' to compilation assets from './fixtures/directory/nested/nestedfile.txt'",
   ],
 }
 `;
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index cc21b05..0742af5 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -127,50 +127,56 @@ exports[`validate options should throw an error on the "patterns" option with "t
    [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
+exports[`validate options should throw an error on the "patterns" option with "undefined" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
+`;
+
 exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options has an unknown property 'unknown'. These properties are valid:
-   object { patterns?, options? }"
+ - options misses the property 'patterns'. Should be:
+   [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)"
 `;
diff --git a/test/globOptions-option.test.js b/test/globOptions-option.test.js
index 4338ad5..595ca35 100644
--- a/test/globOptions-option.test.js
+++ b/test/globOptions-option.test.js
@@ -4,8 +4,45 @@ import { runEmit } from './helpers/run';
 
 const FIXTURES_DIR = path.join(__dirname, 'fixtures');
 
-describe('from option', () => {
-  it('should move files exclude dot files', (done) => {
+describe('globOptions option', () => {
+  // Expected behavior from `globby`/`fast-glob`
+  it('should move files exclude dot files when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.file.txt'],
+      patterns: [
+        {
+          from: '.file.txt',
+          globOptions: {
+            dot: false,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move files exclude dot files when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            dot: false,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move files exclude dot files when "from" is a glob', (done) => {
     runEmit({
       expectedAssetKeys: ['file.txt'],
       patterns: [
@@ -36,9 +73,7 @@ describe('from option', () => {
       .then(done)
       .catch(done);
   });
-});
 
-describe('globOptions ignore option', () => {
   it('should ignore files when "from" is a file', (done) => {
     runEmit({
       expectedErrors: [
@@ -255,4 +290,124 @@ describe('globOptions ignore option', () => {
       .then(done)
       .catch(done);
   });
+
+  it('should ignore the "cwd" option', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            cwd: path.resolve(__dirname, 'fixtures/nested'),
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work with the "deep" option', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            deep: 2,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work with the "markDirectories" option', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            markDirectories: true,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work with the "objectMode" option', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            objectMode: true,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work with the "onlyDirectories" option', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            onlyDirectories: true,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work with the "onlyFiles" option', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            onlyFiles: true,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
 });
diff --git a/test/to-option.test.js b/test/to-option.test.js
index f0f3524..52df880 100644
--- a/test/to-option.test.js
+++ b/test/to-option.test.js
@@ -329,6 +329,20 @@ describe('to option', () => {
         .then(done)
         .catch(done);
     });
+
+    it('should move a file to a new directory when "to" is empty', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: '',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
   });
 
   describe('is a template', () => {
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index 0883af1..34b653e 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -134,6 +134,8 @@ describe('validate options', () => {
         ],
       ],
       failure: [
+        // eslint-disable-next-line no-undefined
+        undefined,
         true,
         'true',
         '',
@@ -276,7 +278,11 @@ describe('validate options', () => {
 
       try {
         // eslint-disable-next-line no-new
-        new CopyPlugin({ [key]: value });
+        new CopyPlugin(
+          key === 'options'
+            ? { patterns: [{ from: 'file.txt' }], [key]: value }
+            : { [key]: value }
+        );
       } catch (errorFromPlugin) {
         if (errorFromPlugin.name !== 'ValidationError') {
           throw errorFromPlugin;