Repository: webpack-contrib/copy-webpack-plugin
Pull request #394 (2019-07-24 12:41:56)

test: refactor
<!--
  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 tests

### Breaking Changes

No

### Additional Info

No

codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/394?src=pr&el=h1) Report
> Merging [#394](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/394?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/commit/ff0c7366d605f2845661a25a86c93eee35a729e0?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@           Coverage Diff           @@
##           master     #394   +/-   ##
=======================================
  Coverage   94.88%   94.88%           
=======================================
  Files           9        9           
  Lines         293      293           
  Branches       78       78           
=======================================
  Hits          278      278           
  Misses         14       14           
  Partials        1        1
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/394?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/processPattern.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/394/diff?src=pr&el=tree#diff-c3JjL3Byb2Nlc3NQYXR0ZXJuLmpz) | `97.91% <ø> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/394?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/394?src=pr&el=footer). Last update [ff0c736...481bd56](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/394?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.gitignore b/.gitignore
index 9b615cd..953a7f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ npm-debug.log*
 /local
 /reports
 /node_modules
+/test/helpers/\[special\?directory\]
 
 .DS_Store
 Thumbs.db
diff --git a/globalSetup.js b/globalSetup.js
new file mode 100644
index 0000000..f28c4be
--- /dev/null
+++ b/globalSetup.js
@@ -0,0 +1,26 @@
+const path = require('path');
+const fs = require('fs');
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+const mkdirp = require('mkdirp');
+
+const removeIllegalCharacterForWindows = require('./test/utils/removeIllegalCharacterForWindows');
+
+const baseDir = path.resolve(__dirname, 'test/helpers');
+
+const specialFiles = {
+  '[special?directory]/nested/nestedfile.txt': '',
+  '[special?directory]/(special-*file).txt': 'special',
+  '[special?directory]/directoryfile.txt': 'new',
+};
+
+module.exports = () => {
+  Object.keys(specialFiles).forEach((originFile) => {
+    const file = removeIllegalCharacterForWindows(originFile);
+    const dir = path.dirname(file);
+
+    mkdirp.sync(path.join(baseDir, dir));
+
+    fs.writeFileSync(path.join(baseDir, file), specialFiles[originFile]);
+  });
+};
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..cd7a966
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  globalSetup: '<rootDir>/globalSetup.js',
+};
diff --git a/src/processPattern.js b/src/processPattern.js
index 6cec72b..447f3aa 100644
--- a/src/processPattern.js
+++ b/src/processPattern.js
@@ -12,6 +12,8 @@ export default function processPattern(globalRef, pattern) {
     {
       cwd: pattern.context,
       follow: true,
+      // Todo in next major release
+      // dot: false
     },
     pattern.globOptions || {}
   );


Test patch:
diff --git a/test/CopyPlugin.test.js b/test/CopyPlugin.test.js
index d54b842..69a9815 100644
--- a/test/CopyPlugin.test.js
+++ b/test/CopyPlugin.test.js
@@ -1,111 +1,21 @@
-import fs from 'fs';
 import path from 'path';
-import zlib from 'zlib';
-
-import findCacheDir from 'find-cache-dir';
-import cacache from 'cacache';
-import isGzip from 'is-gzip';
-import mkdirp from 'mkdirp';
 
 import CopyPlugin from '../src/index';
 
-import removeIllegalCharacterForWindows from './utils/removeIllegalCharacterForWindows';
-
-import { MockCompiler, MockCompilerNoStat } from './utils/mocks';
-import { run, runEmit, runForce, runChange } from './utils/run';
+import { MockCompiler } from './utils/mocks';
+import { run, runEmit, runChange } from './utils/run';
 
 const BUILD_DIR = path.join(__dirname, 'build');
 const HELPER_DIR = path.join(__dirname, 'helpers');
-const TEMP_DIR = path.join(__dirname, 'tempdir');
 
 describe('apply function', () => {
-  const specialFiles = {
-    '[special?directory]/nested/nestedfile.txt': '',
-    '[special?directory]/(special-*file).txt': 'special',
-    '[special?directory]/directoryfile.txt': 'new',
-  };
-
-  const baseDir = path.join(__dirname, 'helpers');
-
-  beforeAll(() => {
-    Object.keys(specialFiles).forEach((originFile) => {
-      const file = removeIllegalCharacterForWindows(originFile);
-      const dir = path.dirname(file);
-
-      mkdirp.sync(path.join(baseDir, dir));
-
-      fs.writeFileSync(path.join(baseDir, file), specialFiles[originFile]);
-    });
-  });
-
-  // Use then and catch explicitly, so errors
-  // aren't seen as unhandled exceptions
-  describe('error handling', () => {
-    it("doesn't throw an error if no patterns are passed", (done) => {
-      runEmit({
-        expectedAssetKeys: [],
-        patterns: undefined, // eslint-disable-line no-undefined
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('throws an error if the patterns are an object', () => {
-      const createPluginWithObject = () => {
-        // eslint-disable-next-line no-new
-        new CopyPlugin({});
-      };
-
-      expect(createPluginWithObject).toThrow(Error);
-    });
-
-    it('throws an error if the patterns are null', () => {
-      const createPluginWithNull = () => {
-        // eslint-disable-next-line no-new
-        new CopyPlugin(null);
-      };
-
-      expect(createPluginWithNull).toThrow(Error);
-    });
-
-    it('throws an error if the "from" path is an empty string', () => {
-      const createPluginWithNull = () => {
-        // eslint-disable-next-line no-new
-        new CopyPlugin({
-          from: '',
-        });
-      };
-
-      expect(createPluginWithNull).toThrow(Error);
-    });
-  });
-
-  describe('with glob in from', () => {
-    it('can use a glob to move a file to the root directory', (done) => {
+  describe('basic', () => {
+    it('should copy a file', (done) => {
       runEmit({
         expectedAssetKeys: ['file.txt'],
         patterns: [
           {
-            from: '*.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can use a bracketed glob to move a file to the root directory', (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'directory/directoryfile.txt',
-          'directory/nested/deep-nested/deepnested.txt',
-          'directory/nested/nestedfile.txt',
-          'file.txt',
-          'noextension',
-        ],
-        patterns: [
-          {
-            from: '{file.txt,noextension,directory/**/*}',
+            from: 'file.txt',
           },
         ],
       })
@@ -113,14 +23,13 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob object to move a file to the root directory', (done) => {
+    it('should copy a file to a new file', (done) => {
       runEmit({
-        expectedAssetKeys: ['file.txt'],
+        expectedAssetKeys: ['newfile.txt'],
         patterns: [
           {
-            from: {
-              glob: '*.txt',
-            },
+            from: 'file.txt',
+            to: 'newfile.txt',
           },
         ],
       })
@@ -128,15 +37,14 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob object to move a file to the root directory and respect glob options', (done) => {
+    it('should copy a file to a new file with context', (done) => {
       runEmit({
-        expectedAssetKeys: ['file.txt'],
+        expectedAssetKeys: ['newfile.txt'],
         patterns: [
           {
-            from: {
-              glob: '*.txt',
-              dot: false,
-            },
+            from: 'directoryfile.txt',
+            context: 'directory',
+            to: 'newfile.txt',
           },
         ],
       })
@@ -144,27 +52,17 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob to move multiple files to the root directory', (done) => {
+    it('should copy files', (done) => {
       runEmit({
         expectedAssetKeys: [
-          '[!]/hello.txt',
-          'binextension.bin',
-          'dir (86)/file.txt',
-          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-          'dir (86)/nesteddir/nestedfile.txt',
-          'file.txt',
-          'file.txt.gz',
-          'directory/directoryfile.txt',
-          'directory/nested/deep-nested/deepnested.txt',
-          'directory/nested/nestedfile.txt',
-          '[special?directory]/directoryfile.txt',
-          '[special?directory]/(special-*file).txt',
-          '[special?directory]/nested/nestedfile.txt',
-          'noextension',
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
         ],
         patterns: [
           {
-            from: '**/*',
+            from: 'directory',
           },
         ],
       })
@@ -172,28 +70,18 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob to move multiple files to a non-root directory', (done) => {
+    it('should copy files to new directory', (done) => {
       runEmit({
         expectedAssetKeys: [
-          'nested/[!]/hello.txt',
-          'nested/binextension.bin',
-          'nested/dir (86)/file.txt',
-          'nested/dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-          'nested/dir (86)/nesteddir/nestedfile.txt',
-          'nested/file.txt',
-          'nested/file.txt.gz',
-          'nested/directory/directoryfile.txt',
-          'nested/directory/nested/deep-nested/deepnested.txt',
-          'nested/directory/nested/nestedfile.txt',
-          'nested/[special?directory]/directoryfile.txt',
-          'nested/[special?directory]/(special-*file).txt',
-          'nested/[special?directory]/nested/nestedfile.txt',
-          'nested/noextension',
+          'newdirectory/.dottedfile',
+          'newdirectory/directoryfile.txt',
+          'newdirectory/nested/deep-nested/deepnested.txt',
+          'newdirectory/nested/nestedfile.txt',
         ],
         patterns: [
           {
-            from: '**/*',
-            to: 'nested',
+            from: 'directory',
+            to: 'newdirectory',
           },
         ],
       })
@@ -201,18 +89,17 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob to move multiple files in a different relative context to a non-root directory', (done) => {
+    it('should copy files to new directory with context', (done) => {
       runEmit({
         expectedAssetKeys: [
-          'nested/directoryfile.txt',
-          'nested/nested/deep-nested/deepnested.txt',
-          'nested/nested/nestedfile.txt',
+          'newdirectory/deep-nested/deepnested.txt',
+          'newdirectory/nestedfile.txt',
         ],
         patterns: [
           {
+            from: 'nested',
             context: 'directory',
-            from: '**/*',
-            to: 'nested',
+            to: 'newdirectory',
           },
         ],
       })
@@ -220,17 +107,16 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a direct glob to move multiple files in a different relative context with special characters', (done) => {
+    it('should copy files using glob', (done) => {
       runEmit({
         expectedAssetKeys: [
-          'directoryfile.txt',
-          '(special-*file).txt',
-          'nested/nestedfile.txt',
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
         ],
         patterns: [
           {
-            context: '[special?directory]',
-            from: { glob: '**/*' },
+            from: 'directory/**/*',
           },
         ],
       })
@@ -238,17 +124,17 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob to move multiple files in a different relative context with special characters', (done) => {
+    it('should copy files using glob to new directory', (done) => {
       runEmit({
         expectedAssetKeys: [
-          'directoryfile.txt',
-          '(special-*file).txt',
-          'nested/nestedfile.txt',
+          'newdirectory/directory/directoryfile.txt',
+          'newdirectory/directory/nested/deep-nested/deepnested.txt',
+          'newdirectory/directory/nested/nestedfile.txt',
         ],
         patterns: [
           {
-            context: '[special?directory]',
-            from: '**/*',
+            from: 'directory/**/*',
+            to: 'newdirectory',
           },
         ],
       })
@@ -256,19 +142,17 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob to flatten multiple files in a relative context to a non-root directory', (done) => {
+    it('should copy files using glob to new directory with context', (done) => {
       runEmit({
         expectedAssetKeys: [
-          'nested/deepnested.txt',
-          'nested/directoryfile.txt',
-          'nested/nestedfile.txt',
+          'newdirectory/nested/deep-nested/deepnested.txt',
+          'newdirectory/nested/nestedfile.txt',
         ],
         patterns: [
           {
+            from: 'nested/**/*',
             context: 'directory',
-            flatten: true,
-            from: '**/*',
-            to: 'nested',
+            to: 'newdirectory',
           },
         ],
       })
@@ -276,18 +160,13 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob to move multiple files in a different absolute context to a non-root directory', (done) => {
+    it('should copy a file to a new file', (done) => {
       runEmit({
-        expectedAssetKeys: [
-          'nested/directoryfile.txt',
-          'nested/nested/deep-nested/deepnested.txt',
-          'nested/nested/nestedfile.txt',
-        ],
+        expectedAssetKeys: ['newfile.txt'],
         patterns: [
           {
-            context: path.join(HELPER_DIR, 'directory'),
-            from: '**/*',
-            to: 'nested',
+            from: 'file.txt',
+            to: 'newfile.txt',
           },
         ],
       })
@@ -295,12 +174,14 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob with a full path to move a file to the root directory', (done) => {
+    it('should copy a file to a new file with context', (done) => {
       runEmit({
-        expectedAssetKeys: ['file.txt'],
+        expectedAssetKeys: ['newfile.txt'],
         patterns: [
           {
-            from: path.join(HELPER_DIR, '*.txt'),
+            from: 'directoryfile.txt',
+            context: 'directory',
+            to: 'newfile.txt',
           },
         ],
       })
@@ -308,53 +189,17 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can use a glob with a full path to move multiple files to the root directory', (done) => {
+    it('should multiple files to a new file', (done) => {
       runEmit({
-        expectedAssetKeys: [
-          '[!]/hello.txt',
-          'file.txt',
-          'directory/directoryfile.txt',
-          'directory/nested/deep-nested/deepnested.txt',
-          'directory/nested/nestedfile.txt',
-          '[special?directory]/directoryfile.txt',
-          '[special?directory]/(special-*file).txt',
-          '[special?directory]/nested/nestedfile.txt',
-          'dir (86)/file.txt',
-          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-          'dir (86)/nesteddir/nestedfile.txt',
-        ],
+        expectedAssetKeys: ['newfile.txt', 'newbinextension.bin'],
         patterns: [
           {
-            from: path.join(HELPER_DIR, '**/*.txt'),
+            from: 'file.txt',
+            to: 'newfile.txt',
           },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can use a glob to move multiple files to a non-root directory with name, hash and ext', (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'nested/[!]/hello-d41d8c.txt',
-          'nested/binextension-d41d8c.bin',
-          'nested/dir (86)/file-d41d8c.txt',
-          'nested/dir (86)/nesteddir/deepnesteddir/deepnesteddir-d41d8c.txt',
-          'nested/dir (86)/nesteddir/nestedfile-d41d8c.txt',
-          'nested/file-22af64.txt',
-          'nested/file.txt-5b311c.gz',
-          'nested/directory/directoryfile-22af64.txt',
-          'nested/directory/nested/deep-nested/deepnested-d41d8c.txt',
-          'nested/directory/nested/nestedfile-d41d8c.txt',
-          'nested/[special?directory]/(special-*file)-0bd650.txt',
-          'nested/[special?directory]/directoryfile-22af64.txt',
-          'nested/[special?directory]/nested/nestedfile-d41d8c.txt',
-          'nested/noextension-d41d8c',
-        ],
-        patterns: [
           {
-            from: '**/*',
-            to: 'nested/[path][name]-[hash:6].[ext]',
+            from: 'binextension.bin',
+            to: 'newbinextension.bin',
           },
         ],
       })
@@ -362,118 +207,60 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can flatten or normalize glob matches', (done) => {
+    it('should copy multiple files with same "from"', (done) => {
       runEmit({
-        expectedAssetKeys: [
-          '[!]-hello.txt',
-          '[special?directory]-(special-*file).txt',
-          '[special?directory]-directoryfile.txt',
-          'dir (86)-file.txt',
-          'directory-directoryfile.txt',
-        ],
+        expectedAssetKeys: ['first/file.txt', 'second/file.txt'],
         patterns: [
           {
-            from: '*/*.*',
-            test: `([^\\${path.sep}]+)\\${path.sep}([^\\${path.sep}]+)\\.\\w+$`,
-            to: '[1]-[2].[ext]',
+            from: 'file.txt',
+            to: 'first/file.txt',
           },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('adds the context directory to the watch list when using glob', (done) => {
-      run({
-        patterns: [
           {
-            from: 'directory/**/*',
+            from: 'file.txt',
+            to: 'second/file.txt',
           },
         ],
       })
-        .then((compilation) => {
-          expect(
-            Array.from(compilation.contextDependencies)
-              .map((contextDependency) => contextDependency)
-              .sort()
-          ).toEqual([path.join(HELPER_DIR, 'directory')].sort());
-        })
         .then(done)
         .catch(done);
     });
 
-    it('does not add the directory to the watch list when glob is a file', (done) => {
-      run({
-        patterns: [
-          {
-            from: {
-              glob: 'directory/directoryfile.txt',
-            },
-          },
-        ],
+    it('should works with multiple patterns as String', (done) => {
+      runEmit({
+        expectedAssetKeys: ['binextension.bin', 'file.txt', 'noextension'],
+        patterns: ['binextension.bin', 'file.txt', 'noextension'],
       })
-        .then((compilation) => {
-          const absFrom = path.resolve(HELPER_DIR, 'directory');
-          expect(compilation.contextDependencies).not.toContain(absFrom);
-        })
         .then(done)
         .catch(done);
     });
 
-    it('can use a glob to move a file to the root directory from symbolic link', (done) => {
+    it('should works with multiple patterns as Object', (done) => {
       runEmit({
-        // Windows doesn't support symbolic link
-        symlink: true,
-        expectedAssetKeys:
-          process.platform === 'win32'
-            ? []
-            : [
-                'symlink/directory-ln/file.txt',
-                'symlink/directory-ln/nested-directory/file-in-nested-directory.txt',
-                'symlink/directory/file.txt',
-                'symlink/directory/nested-directory/file-in-nested-directory.txt',
-                'symlink/file-ln.txt',
-                'symlink/file.txt',
-              ],
+        expectedAssetKeys: ['binextension.bin', 'file.txt', 'noextension'],
         patterns: [
           {
-            from: 'symlink/**/*.txt',
+            from: 'binextension.bin',
           },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-  });
-
-  describe('with file in from', () => {
-    it('can move a file to the root directory', (done) => {
-      runEmit({
-        expectedAssetKeys: ['file.txt'],
-        patterns: [
           {
             from: 'file.txt',
           },
+          {
+            from: 'noextension',
+          },
         ],
       })
         .then(done)
         .catch(done);
     });
+  });
 
-    it('can transform a file', (done) => {
+  describe('difference path segment separation', () => {
+    it('should work with linux path segment separation path when "from" is glob', (done) => {
       runEmit({
-        expectedAssetKeys: ['file.txt'],
-        expectedAssetContent: {
-          'file.txt': 'newchanged',
-        },
+        expectedAssetKeys: ['directory/nested/nestedfile.txt'],
         patterns: [
           {
-            from: 'file.txt',
-            transform(content, absoluteFrom) {
-              expect(absoluteFrom).toBe(path.join(HELPER_DIR, 'file.txt'));
-
-              return `${content}changed`;
-            },
+            from: 'directory/nested/*',
           },
         ],
       })
@@ -481,17 +268,14 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('warns when file not found', (done) => {
+    it('should work with windows path segment separation path when "from" is glob', (done) => {
       runEmit({
-        expectedAssetKeys: [],
-        expectedWarnings: [
-          new Error(
-            `unable to locate 'nonexistent.txt' at '${HELPER_DIR}${path.sep}nonexistent.txt'`
-          ),
-        ],
+        expectedAssetKeys: ['directory/nested/nestedfile.txt'],
         patterns: [
           {
-            from: 'nonexistent.txt',
+            from: {
+              glob: 'directory\\nested\\*',
+            },
           },
         ],
       })
@@ -499,20 +283,14 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('warns when file not found and stats is undefined', (done) => {
+    it('should work with mixed path segment separation path when "from" is glob', (done) => {
       runEmit({
-        compiler: new MockCompilerNoStat(),
-        expectedAssetKeys: [],
-        expectedWarnings: [
-          new Error(
-            `unable to locate 'nonexistent.txt' at '${HELPER_DIR}${path.sep}nonexistent.txt'`
-          ),
-        ],
+        expectedAssetKeys: ['directory/nested/nestedfile.txt'],
         patterns: [
           {
-            from: 'nonexistent.txt',
-            to: '.',
-            toType: 'dir',
+            from: {
+              glob: 'directory/nested\\*',
+            },
           },
         ],
       })
@@ -520,17 +298,20 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('warns when tranform failed', (done) => {
+    it('should exclude path with linux path segment separators', (done) => {
       runEmit({
-        expectedAssetKeys: [],
-        expectedErrors: ['a failure happened'],
+        expectedAssetKeys: [
+          '[!]/hello.txt',
+          '[special?directory]/(special-*file).txt',
+          '[special?directory]/directoryfile.txt',
+          '[special?directory]/nested/nestedfile.txt',
+          'dir (86)/file.txt',
+          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
+          'dir (86)/nesteddir/nestedfile.txt',
+        ],
         patterns: [
           {
-            from: 'file.txt',
-            transform() {
-              // eslint-disable-next-line no-throw-literal
-              throw 'a failure happened';
-            },
+            from: '!(directory)/**/*.txt',
           },
         ],
       })
@@ -538,855 +319,113 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('warns when pattern is empty', (done) => {
+    it('should exclude path with windows path segment separators', (done) => {
       runEmit({
         expectedAssetKeys: [
-          '.file.txt',
           '[!]/hello.txt',
           '[special?directory]/(special-*file).txt',
           '[special?directory]/directoryfile.txt',
           '[special?directory]/nested/nestedfile.txt',
-          'binextension.bin',
           'dir (86)/file.txt',
           'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
           'dir (86)/nesteddir/nestedfile.txt',
-          'directory/.dottedfile',
-          'directory/directoryfile.txt',
-          'directory/nested/deep-nested/deepnested.txt',
-          'directory/nested/nestedfile.txt',
-          'file.txt',
-          'file.txt.gz',
-          'noextension',
         ],
-        expectedErrors: [new Error(`path "from" cannot be empty string`)],
         patterns: [
           {
-            from: '',
+            from: '!(directory)\\**\\*.txt',
           },
         ],
       })
         .then(done)
         .catch(done);
     });
+  });
 
-    it('can use an absolute path to move a file to the root directory', (done) => {
-      const absolutePath = path.resolve(HELPER_DIR, 'file.txt');
-
+  describe('errors', () => {
+    it('should not throw an error if no patterns are passed', (done) => {
       runEmit({
-        expectedAssetKeys: ['file.txt'],
-        patterns: [
-          {
-            from: absolutePath,
-          },
-        ],
+        expectedAssetKeys: [],
+        patterns: undefined, // eslint-disable-line no-undefined
       })
         .then(done)
         .catch(done);
     });
 
-    it('can move a file to a new directory without a forward slash', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newdirectory/file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to the root directory using an absolute to', (done) => {
-      runEmit({
-        expectedAssetKeys: ['file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: BUILD_DIR,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('allows absolute to if outpath is defined with webpack-dev-server', (done) => {
-      runEmit({
-        compiler: new MockCompiler({
-          outputPath: '/',
-          devServer: {
-            outputPath: BUILD_DIR,
-          },
-        }),
-        expectedAssetKeys: ['file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: BUILD_DIR,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("throws an error when output path isn't defined with webpack-dev-server", (done) => {
-      runEmit({
-        compiler: new MockCompiler({
-          outputPath: '/',
-        }),
-        skipAssetsTesting: true,
-        expectedErrors: [
-          new Error(
-            'using older versions of webpack-dev-server, devServer.outputPath must be defined to write to absolute paths'
-          ),
-        ],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: BUILD_DIR,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new directory using an absolute to', (done) => {
-      runEmit({
-        expectedAssetKeys: ['../tempdir/file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: TEMP_DIR,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new file using an absolute to', (done) => {
-      const absolutePath = path.resolve(TEMP_DIR, 'newfile.txt');
-
-      runEmit({
-        expectedAssetKeys: ['../tempdir/newfile.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: absolutePath,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new directory with a forward slash', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newdirectory/file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: 'newdirectory/',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file with a context containing special characters', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directoryfile.txt'],
-        patterns: [
-          {
-            from: 'directoryfile.txt',
-            context: '[special?directory]',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file with special characters with a context containing special characters', (done) => {
-      runEmit({
-        expectedAssetKeys: ['(special-*file).txt'],
-        patterns: [
-          {
-            from: '(special-*file).txt',
-            context: '[special?directory]',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new directory with an extension', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newdirectory.ext/file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: 'newdirectory.ext',
-            toType: 'dir',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new directory with an extension and path separator at end', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newdirectory.ext/file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: `newdirectory.ext${path.sep}`,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new file with a different name', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newname.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: 'newname.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file to a new file with no extension', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newname'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: 'newname',
-            toType: 'file',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file without an extension to a file using a template', (done) => {
-      runEmit({
-        expectedAssetKeys: ['noextension.newext'],
-        patterns: [
-          {
-            from: 'noextension',
-            to: '[name][ext].newext',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file with a ".bin" extension using a template', (done) => {
-      runEmit({
-        expectedAssetKeys: ['binextension.bin'],
-        patterns: [
-          {
-            from: 'binextension.bin',
-            to: '[name].[ext]',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a nested file to the root directory', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directoryfile.txt'],
-        patterns: [
-          {
-            from: 'directory/directoryfile.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can use an absolute path to move a nested file to the root directory', (done) => {
-      const absolutePath = path.resolve(
-        HELPER_DIR,
-        'directory',
-        'directoryfile.txt'
-      );
-
-      runEmit({
-        expectedAssetKeys: ['directoryfile.txt'],
-        patterns: [
-          {
-            from: absolutePath,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a nested file to a new directory', (done) => {
-      runEmit({
-        expectedAssetKeys: ['newdirectory/directoryfile.txt'],
-        patterns: [
-          {
-            from: 'directory/directoryfile.txt',
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can use an absolute path to move a nested file to a new directory', (done) => {
-      const absolutePath = path.resolve(
-        HELPER_DIR,
-        'directory',
-        'directoryfile.txt'
-      );
-
-      runEmit({
-        expectedAssetKeys: ['newdirectory/directoryfile.txt'],
-        patterns: [
-          {
-            from: absolutePath,
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("won't overwrite a file already in the compilation", (done) => {
-      runForce({
-        existingAsset: 'file.txt',
-        expectedAssetContent: {
-          'file.txt': 'existing',
-        },
-        patterns: [
-          {
-            from: 'file.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can force overwrite of a file already in the compilation', (done) => {
-      runForce({
-        existingAsset: 'file.txt',
-        expectedAssetContent: {
-          'file.txt': 'new',
-        },
-        patterns: [
-          {
-            force: true,
-            from: 'file.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('adds the file to the watch list', (done) => {
-      run({
-        patterns: [
-          {
-            from: 'file.txt',
-          },
-        ],
-      })
-        .then((compilation) => {
-          const absFrom = path.join(HELPER_DIR, 'file.txt');
-
-          expect(Array.from(compilation.fileDependencies).sort()).toEqual(
-            [absFrom].sort()
-          );
-        })
-        .then(done)
-        .catch(done);
-    });
-
-    it('only include files that have changed', (done) => {
-      runChange({
-        expectedAssetKeys: ['tempfile1.txt'],
-        newFileLoc1: path.join(HELPER_DIR, 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'tempfile2.txt'),
-        patterns: [
-          {
-            from: 'tempfile1.txt',
-          },
-          {
-            from: 'tempfile2.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('ignores files in pattern', (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          '[!]/hello.txt',
-          'binextension.bin',
-          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-          'dir (86)/nesteddir/nestedfile.txt',
-          'directory/directoryfile.txt',
-          'directory/nested/deep-nested/deepnested.txt',
-          'directory/nested/nestedfile.txt',
-          '[special?directory]/directoryfile.txt',
-          '[special?directory]/(special-*file).txt',
-          '[special?directory]/nested/nestedfile.txt',
-          'noextension',
-        ],
-        patterns: [
-          {
-            from: '**/*',
-            ignore: ['file.*', 'file-in-nested-directory.*'],
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('allows pattern to contain name, hash or ext', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/directoryfile-22af64.txt'],
-        patterns: [
-          {
-            from: 'directory/directoryfile.txt',
-            to: 'directory/[name]-[hash:6].[ext]',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('allows pattern to contain contenthash', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/22af64.txt'],
-        patterns: [
-          {
-            from: 'directory/directoryfile.txt',
-            to: 'directory/[contenthash:6].txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('allows pattern to contain custoh `contenthash` digest', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/c2a6.txt'],
-        patterns: [
-          {
-            from: 'directory/directoryfile.txt',
-            to: 'directory/[sha1:contenthash:hex:4].txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('allows pattern to contain `hashType` without `hash` or `contenthash`', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/[md5::base64:20].txt'],
-        patterns: [
-          {
-            from: 'directory/directoryfile.txt',
-            to: 'directory/[md5::base64:20].txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('transform with promise', (done) => {
-      runEmit({
-        expectedAssetKeys: ['file.txt'],
-        expectedAssetContent: {
-          'file.txt': 'newchanged!',
-        },
-        patterns: [
-          {
-            from: 'file.txt',
-            transform(content) {
-              return new Promise((resolve) => {
-                resolve(`${content}changed!`);
-              });
-            },
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('same file to multiple targets', (done) => {
-      runEmit({
-        expectedAssetKeys: ['first/file.txt', 'second/file.txt'],
-        patterns: [
-          {
-            from: 'file.txt',
-            to: 'first/file.txt',
-          },
-          {
-            from: 'file.txt',
-            to: 'second/file.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can move a file (symbolic link) to the root directory', (done) => {
-      // Windows doesn't support symbolic link
-      runEmit({
-        symlink: true,
-        expectedAssetKeys: process.platform === 'win32' ? [] : ['file-ln.txt'],
-        patterns: [
-          {
-            from: 'symlink/file-ln.txt',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-  });
-
-  describe('with directory in from', () => {
-    it("can move a directory's contents to the root directory", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          '.dottedfile',
-          'directoryfile.txt',
-          'nested/deep-nested/deepnested.txt',
-          'nested/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: 'directory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("can move a directory's contents to the root directory using from with special characters", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'directoryfile.txt',
-          '(special-*file).txt',
-          'nested/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from:
-              path.sep === '/' ? '[special?directory]' : '[specialdirectory]',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("can move a directory's contents to the root directory using context with special characters", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'directoryfile.txt',
-          '(special-*file).txt',
-          'nested/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: '.',
-            context: '[special?directory]',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('warns when directory not found', (done) => {
-      runEmit({
-        expectedAssetKeys: [],
-        expectedWarnings: [
-          new Error(
-            `unable to locate 'nonexistent' at '${HELPER_DIR}${path.sep}nonexistent'`
-          ),
-        ],
-        patterns: [
-          {
-            from: 'nonexistent',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("can use an absolute path to move a directory's contents to the root directory", (done) => {
-      const absolutePath = path.resolve(HELPER_DIR, 'directory');
-
-      runEmit({
-        expectedAssetKeys: [
-          '.dottedfile',
-          'directoryfile.txt',
-          'nested/deep-nested/deepnested.txt',
-          'nested/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: absolutePath,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("can move a directory's contents to a new directory", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'newdirectory/.dottedfile',
-          'newdirectory/directoryfile.txt',
-          'newdirectory/nested/deep-nested/deepnested.txt',
-          'newdirectory/nested/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: 'directory',
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
+    it('should throw an error if the patterns are an object', () => {
+      const createPluginWithObject = () => {
+        // eslint-disable-next-line no-new
+        new CopyPlugin({});
+      };
 
-    it("can move a directory's contents to a new directory using a pattern context", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'newdirectory/deep-nested/deepnested.txt',
-          'newdirectory/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            context: 'directory',
-            from: 'nested',
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
+      expect(createPluginWithObject).toThrow(Error);
     });
 
-    it("can flatten a directory's contents to a new directory", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'newdirectory/.dottedfile',
-          'newdirectory/deepnested.txt',
-          'newdirectory/directoryfile.txt',
-          'newdirectory/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            flatten: true,
-            from: 'directory',
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
+    it('should throw an error if the patterns are null', () => {
+      const createPluginWithNull = () => {
+        // eslint-disable-next-line no-new
+        new CopyPlugin(null);
+      };
 
-    it("can move a directory's contents to a new directory using an absolute to", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          '../tempdir/.dottedfile',
-          '../tempdir/directoryfile.txt',
-          '../tempdir/nested/deep-nested/deepnested.txt',
-          '../tempdir/nested/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: 'directory',
-            to: TEMP_DIR,
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
+      expect(createPluginWithNull).toThrow(Error);
     });
 
-    it("can move a nested directory's contents to the root directory", (done) => {
-      runEmit({
-        expectedAssetKeys: ['deep-nested/deepnested.txt', 'nestedfile.txt'],
-        patterns: [
-          {
-            from: 'directory/nested',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
+    it('should throw an error if the "from" path is an empty string', () => {
+      const createPluginWithNull = () => {
+        // eslint-disable-next-line no-new
+        new CopyPlugin({
+          from: '',
+        });
+      };
 
-    it("can move a nested directory's contents to a new directory", (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          'newdirectory/deep-nested/deepnested.txt',
-          'newdirectory/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: 'directory/nested',
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
+      expect(createPluginWithNull).toThrow(Error);
     });
 
-    it("can use an absolute path to move a nested directory's contents to a new directory", (done) => {
-      const absolutePath = path.resolve(HELPER_DIR, 'directory', 'nested');
-
+    it('should warn when pattern is empty', (done) => {
       runEmit({
         expectedAssetKeys: [
-          'newdirectory/deep-nested/deepnested.txt',
-          'newdirectory/nestedfile.txt',
-        ],
-        patterns: [
-          {
-            from: absolutePath,
-            to: 'newdirectory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it("won't overwrite a file already in the compilation", (done) => {
-      runForce({
-        existingAsset: 'directoryfile.txt',
-        expectedAssetContent: {
-          'directoryfile.txt': 'existing',
-        },
-        patterns: [
-          {
-            from: 'directory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('can force overwrite of a file already in the compilation', (done) => {
-      runForce({
-        existingAsset: 'directoryfile.txt',
-        expectedAssetContent: {
-          'directoryfile.txt': 'new',
-        },
-        patterns: [
-          {
-            force: true,
-            from: 'directory',
-          },
-        ],
-      })
-        .then(done)
-        .catch(done);
-    });
-
-    it('adds the context directory to the watch list', (done) => {
-      run({
-        patterns: [
-          {
-            from: 'directory',
-          },
+          '.file.txt',
+          '[!]/hello.txt',
+          '[special?directory]/(special-*file).txt',
+          '[special?directory]/directoryfile.txt',
+          '[special?directory]/nested/nestedfile.txt',
+          'binextension.bin',
+          'dir (86)/file.txt',
+          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
+          'dir (86)/nesteddir/nestedfile.txt',
+          'directory/.dottedfile',
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+          'file.txt',
+          'file.txt.gz',
+          'noextension',
         ],
-      })
-        .then((compilation) => {
-          const absFrom = path.resolve(HELPER_DIR, 'directory');
-          expect(Array.from(compilation.contextDependencies).sort()).toEqual(
-            [absFrom].sort()
-          );
-        })
-        .then(done)
-        .catch(done);
-    });
-
-    it('only include files that have changed', (done) => {
-      runChange({
-        expectedAssetKeys: ['tempfile1.txt'],
-        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        expectedErrors: [new Error(`path "from" cannot be empty string`)],
         patterns: [
           {
-            from: 'directory',
+            from: '',
           },
         ],
       })
         .then(done)
         .catch(done);
     });
+  });
 
-    it('include all files if copyUnmodified is true', (done) => {
-      runChange({
-        expectedAssetKeys: [
-          '.dottedfile',
-          'directoryfile.txt',
-          'nested/deep-nested/deepnested.txt',
-          'nested/nestedfile.txt',
-          'tempfile1.txt',
-          'tempfile2.txt',
-        ],
-        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
-        options: {
-          copyUnmodified: true,
-        },
+  describe('dev server', () => {
+    it('should work with absolute to if outpath is defined with webpack-dev-server', (done) => {
+      runEmit({
+        compiler: new MockCompiler({
+          outputPath: '/',
+          devServer: {
+            outputPath: BUILD_DIR,
+          },
+        }),
+        expectedAssetKeys: ['file.txt'],
         patterns: [
           {
-            from: 'directory',
+            from: 'file.txt',
+            to: BUILD_DIR,
           },
         ],
       })
@@ -1394,107 +433,117 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can move multiple files to a non-root directory with name, hash and ext', (done) => {
+    it("should throw an error when output path isn't defined with webpack-dev-server", (done) => {
       runEmit({
-        expectedAssetKeys: [
-          'nested/.dottedfile-79d39f',
-          'nested/directoryfile-22af64.txt',
-          'nested/nested/deep-nested/deepnested-d41d8c.txt',
-          'nested/nested/nestedfile-d41d8c.txt',
+        compiler: new MockCompiler({
+          outputPath: '/',
+        }),
+        skipAssetsTesting: true,
+        expectedErrors: [
+          new Error(
+            'using older versions of webpack-dev-server, devServer.outputPath must be defined to write to absolute paths'
+          ),
         ],
         patterns: [
           {
-            from: 'directory',
-            to: 'nested/[path][name]-[hash:6].[ext]',
+            from: 'file.txt',
+            to: BUILD_DIR,
           },
         ],
       })
         .then(done)
         .catch(done);
     });
+  });
 
-    it('can move multiple files to a non-root directory with [1]', (done) => {
-      runEmit({
-        expectedAssetKeys: ['nested/txt'],
+  describe('watch mode', () => {
+    it('should add the file to the watch list when "from" is a file', (done) => {
+      run({
         patterns: [
           {
-            from: 'directory/nested/deep-nested',
-            to: 'nested/[1]',
-            test: /\.([^.]*)$/,
+            from: 'file.txt',
           },
         ],
       })
+        .then((compilation) => {
+          const absFrom = path.join(HELPER_DIR, 'file.txt');
+
+          expect(Array.from(compilation.fileDependencies).sort()).toEqual(
+            [absFrom].sort()
+          );
+        })
         .then(done)
         .catch(done);
     });
 
-    it("can move a directory's contents to the root directory from symbolic link", (done) => {
-      runEmit({
-        // Windows doesn't support symbolic link
-        symlink: true,
-        expectedAssetKeys:
-          process.platform === 'win32'
-            ? []
-            : ['file.txt', 'nested-directory/file-in-nested-directory.txt'],
+    it('should add a directory to the watch list when "from" is a directory', (done) => {
+      run({
         patterns: [
           {
-            from: 'symlink/directory-ln',
+            from: 'directory',
           },
         ],
       })
-        .then(done)
-        .catch(done);
-    });
-  });
+        .then((compilation) => {
+          const absFrom = path.join(HELPER_DIR, 'directory');
 
-  describe('with simple string patterns', () => {
-    it('can move multiple files', (done) => {
-      runEmit({
-        expectedAssetKeys: ['binextension.bin', 'file.txt', 'noextension'],
-        patterns: ['binextension.bin', 'file.txt', 'noextension'],
-      })
+          expect(Array.from(compilation.contextDependencies).sort()).toEqual(
+            [absFrom].sort()
+          );
+        })
         .then(done)
         .catch(done);
     });
-  });
 
-  describe('with difference path segment separation', () => {
-    it('can normalize backslash path with glob in from', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/nested/nestedfile.txt'],
+    it('should add a directory to the watch list when "from" is a glob', (done) => {
+      run({
         patterns: [
           {
-            from: {
-              glob: 'directory\\nested\\*',
-            },
+            from: 'directory/**/*',
           },
         ],
       })
+        .then((compilation) => {
+          expect(
+            Array.from(compilation.contextDependencies)
+              .map((contextDependency) => contextDependency)
+              .sort()
+          ).toEqual([path.join(HELPER_DIR, 'directory')].sort());
+        })
         .then(done)
         .catch(done);
     });
 
-    it('can normalize backslash path with glob in from (mixed path segment separation)', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/nested/nestedfile.txt'],
+    it('should not add the directory to the watch list when glob is a file', (done) => {
+      run({
         patterns: [
           {
             from: {
-              glob: 'directory/nested\\*',
+              glob: 'directory/directoryfile.txt',
             },
           },
         ],
       })
+        .then((compilation) => {
+          const absFrom = path.join(HELPER_DIR, 'directory');
+
+          expect(compilation.contextDependencies).not.toContain(absFrom);
+        })
         .then(done)
         .catch(done);
     });
 
-    it('can normalize backslash path with glob in from (simple)', (done) => {
-      runEmit({
-        expectedAssetKeys: ['directory/nested/nestedfile.txt'],
+    it('only include files that have changed', (done) => {
+      runChange({
+        expectedAssetKeys: ['tempfile1.txt'],
+        newFileLoc1: path.join(HELPER_DIR, 'watch', 'tempfile1.txt'),
+        newFileLoc2: path.join(HELPER_DIR, 'watch', 'tempfile2.txt'),
         patterns: [
           {
-            from: 'directory\\nested\\*',
+            from: 'tempfile1.txt',
+          },
+          {
+            from: 'tempfile2.txt',
           },
         ],
       })
@@ -1502,20 +551,24 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can exclude path', (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          '[!]/hello.txt',
-          '[special?directory]/(special-*file).txt',
-          '[special?directory]/directoryfile.txt',
-          '[special?directory]/nested/nestedfile.txt',
-          'dir (86)/file.txt',
-          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-          'dir (86)/nesteddir/nestedfile.txt',
-        ],
+    it('only include files that have changed', (done) => {
+      runChange({
+        expectedAssetKeys: ['tempfile1.txt'],
+        newFileLoc1: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile1.txt'
+        ),
+        newFileLoc2: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile2.txt'
+        ),
         patterns: [
           {
-            from: '!(directory)/**/*.txt',
+            from: 'directory',
           },
         ],
       })
@@ -1523,34 +576,49 @@ describe('apply function', () => {
         .catch(done);
     });
 
-    it('can exclude path with backslash path', (done) => {
-      runEmit({
-        expectedAssetKeys: [
-          '[!]/hello.txt',
-          '[special?directory]/(special-*file).txt',
-          '[special?directory]/directoryfile.txt',
-          '[special?directory]/nested/nestedfile.txt',
-          'dir (86)/file.txt',
-          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-          'dir (86)/nesteddir/nestedfile.txt',
-        ],
+    it('include all files if copyUnmodified is true', (done) => {
+      runChange({
+        expectedAssetKeys: ['tempfile1.txt', 'tempfile2.txt', '.gitkeep'],
+        newFileLoc1: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile1.txt'
+        ),
+        newFileLoc2: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile2.txt'
+        ),
+        options: {
+          copyUnmodified: true,
+        },
         patterns: [
           {
-            from: '!(directory)\\**\\*.txt',
+            from: 'directory',
           },
         ],
       })
         .then(done)
         .catch(done);
     });
-  });
 
-  describe('modified files', () => {
     it('copy only changed files', (done) => {
       runChange({
         expectedAssetKeys: ['dest1/tempfile1.txt'],
-        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        newFileLoc1: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile1.txt'
+        ),
+        newFileLoc2: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile2.txt'
+        ),
         patterns: [
           {
             context: 'directory',
@@ -1566,8 +634,18 @@ describe('apply function', () => {
     it('copy only changed files (multiple patterns)', (done) => {
       runChange({
         expectedAssetKeys: ['dest1/tempfile1.txt', 'dest2/tempfile1.txt'],
-        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        newFileLoc1: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile1.txt'
+        ),
+        newFileLoc2: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile2.txt'
+        ),
         patterns: [
           {
             context: 'directory',
@@ -1591,8 +669,13 @@ describe('apply function', () => {
           'dest1/tempfile1.txt',
           'dest2/directory/tempfile1.txt',
         ],
-        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'tempfile2.txt'),
+        newFileLoc1: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile1.txt'
+        ),
+        newFileLoc2: path.join(HELPER_DIR, 'watch', 'tempfile2.txt'),
         patterns: [
           {
             context: 'directory',
@@ -1615,8 +698,13 @@ describe('apply function', () => {
           'dest1/directory/tempfile1.txt',
           'dest2/tempfile1.txt',
         ],
-        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'tempfile2.txt'),
+        newFileLoc1: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile1.txt'
+        ),
+        newFileLoc2: path.join(HELPER_DIR, 'watch', 'tempfile2.txt'),
         patterns: [
           {
             from: '**/*.txt',
@@ -1636,8 +724,13 @@ describe('apply function', () => {
     it('copy only changed files (multiple patterns with difference context 2)', (done) => {
       runChange({
         expectedAssetKeys: ['dest1/tempfile1.txt'],
-        newFileLoc1: path.join(HELPER_DIR, 'tempfile1.txt'),
-        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        newFileLoc1: path.join(HELPER_DIR, 'watch', 'tempfile1.txt'),
+        newFileLoc2: path.join(
+          HELPER_DIR,
+          'watch',
+          'directory',
+          'tempfile2.txt'
+        ),
         patterns: [
           {
             from: '**/*.txt',
@@ -1654,543 +747,4 @@ describe('apply function', () => {
         .catch(done);
     });
   });
-
-  describe('options', () => {
-    describe('ignore', () => {
-      it('ignores files when from is a file', (done) => {
-        runEmit({
-          expectedAssetKeys: ['directoryfile.txt'],
-          options: {
-            ignore: ['file.*'],
-          },
-          patterns: [
-            {
-              from: 'file.txt',
-            },
-            {
-              from: 'directory/directoryfile.txt',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('ignores files when from is a directory', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            '.dottedfile',
-            'directoryfile.txt',
-            'nested/deep-nested/deepnested.txt',
-          ],
-          options: {
-            ignore: ['*/nestedfile.*'],
-          },
-          patterns: [
-            {
-              from: 'directory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('ignores files with a certain extension', (done) => {
-        runEmit({
-          expectedAssetKeys: ['.dottedfile'],
-          options: {
-            ignore: ['*.txt'],
-          },
-          patterns: [
-            {
-              from: 'directory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('ignores files that start with a dot', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            '[!]/hello.txt',
-            'binextension.bin',
-            'dir (86)/file.txt',
-            'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-            'dir (86)/nesteddir/nestedfile.txt',
-            'file.txt',
-            'file.txt.gz',
-            'directory/directoryfile.txt',
-            'directory/nested/deep-nested/deepnested.txt',
-            'directory/nested/nestedfile.txt',
-            '[special?directory]/directoryfile.txt',
-            '[special?directory]/(special-*file).txt',
-            '[special?directory]/nested/nestedfile.txt',
-            'noextension',
-          ],
-          options: {
-            ignore: ['.dottedfile', '.file.txt'],
-          },
-          patterns: [
-            {
-              from: '.',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('ignores all files except those with dots', (done) => {
-        runEmit({
-          expectedAssetKeys: ['.file.txt', 'directory/.dottedfile'],
-          options: {
-            ignore: [
-              {
-                dot: false,
-                glob: '**/*',
-              },
-            ],
-          },
-          patterns: [
-            {
-              from: '.',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('ignores all files even if they start with a dot', (done) => {
-        runEmit({
-          expectedAssetKeys: [],
-          options: {
-            ignore: ['**/*'],
-          },
-          patterns: [
-            {
-              from: '.',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('ignores nested directory', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            '.file.txt',
-            '[!]/hello.txt',
-            'binextension.bin',
-            'dir (86)/file.txt',
-            'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-            'dir (86)/nesteddir/nestedfile.txt',
-            'file.txt',
-            'file.txt.gz',
-            'noextension',
-          ],
-          options: {
-            ignore: [
-              'directory/**/*',
-              `[[]special${
-                process.platform === 'win32' ? '' : '[?]'
-              }directory]/**/*`,
-            ],
-          },
-          patterns: [
-            {
-              from: '.',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      if (path.sep === '/') {
-        it('ignores nested directory(can use "\\" to escape if path.sep is "/")', (done) => {
-          runEmit({
-            expectedAssetKeys: [
-              '.file.txt',
-              '[!]/hello.txt',
-              'binextension.bin',
-              'dir (86)/file.txt',
-              'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
-              'dir (86)/nesteddir/nestedfile.txt',
-              'file.txt',
-              'file.txt.gz',
-              'noextension',
-            ],
-            options: {
-              ignore: ['directory/**/*', '\\[special\\?directory\\]/**/*'],
-            },
-            patterns: [
-              {
-                from: '.',
-              },
-            ],
-          })
-            .then(done)
-            .catch(done);
-        });
-      }
-
-      it('ignores nested directory (glob)', (done) => {
-        runEmit({
-          expectedAssetKeys: ['.dottedfile', 'directoryfile.txt'],
-          options: {
-            ignore: ['nested/**/*'],
-          },
-          patterns: [
-            {
-              from: 'directory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-    });
-
-    describe('context', () => {
-      it('overrides webpack config context with absolute path', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            'newdirectory/deep-nested/deepnested.txt',
-            'newdirectory/nestedfile.txt',
-          ],
-          options: {
-            context: path.resolve(HELPER_DIR, 'directory'),
-          },
-          patterns: [
-            {
-              from: 'nested',
-              to: 'newdirectory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('overrides webpack config context with relative path', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            'newdirectory/deep-nested/deepnested.txt',
-            'newdirectory/nestedfile.txt',
-          ],
-          options: {
-            context: 'directory',
-          },
-          patterns: [
-            {
-              from: 'nested',
-              to: 'newdirectory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('is overridden by pattern context', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            'newdirectory/deep-nested/deepnested.txt',
-            'newdirectory/nestedfile.txt',
-          ],
-          options: {
-            context: 'directory',
-          },
-          patterns: [
-            {
-              context: 'nested',
-              from: '.',
-              to: 'newdirectory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-
-      it('overrides webpack config context with absolute path', (done) => {
-        runEmit({
-          expectedAssetKeys: [
-            'newdirectory/file.txt',
-            'newdirectory/nesteddir/deepnesteddir/deepnesteddir.txt',
-            'newdirectory/nesteddir/nestedfile.txt',
-          ],
-          options: {
-            context: path.resolve(HELPER_DIR, 'dir (86)'),
-          },
-          patterns: [
-            {
-              from: '**/*',
-              to: 'newdirectory',
-            },
-          ],
-        })
-          .then(done)
-          .catch(done);
-      });
-    });
-
-    describe('cache', () => {
-      const cacheDir = findCacheDir({ name: 'copy-webpack-plugin' });
-
-      beforeEach(() => cacache.rm.all(cacheDir));
-
-      it('file should be cached', (done) => {
-        const newContent = 'newchanged!';
-        const from = 'file.txt';
-
-        runEmit({
-          expectedAssetKeys: ['file.txt'],
-          expectedAssetContent: {
-            'file.txt': newContent,
-          },
-          patterns: [
-            {
-              from,
-              cache: true,
-              transform: function transform(content) {
-                return new Promise((resolve) => {
-                  resolve(`${content}changed!`);
-                });
-              },
-            },
-          ],
-        })
-          .then(() =>
-            cacache.ls(cacheDir).then((cacheEntries) => {
-              const cacheKeys = Object.keys(cacheEntries);
-
-              expect(cacheKeys).toHaveLength(1);
-
-              cacheKeys.forEach((cacheKey) => {
-                // eslint-disable-next-line no-new-func
-                const cacheEntry = new Function(
-                  `'use strict'\nreturn ${cacheKey}`
-                )();
-
-                expect(cacheEntry.pattern.from).toBe(from);
-              });
-            })
-          )
-          .then(done)
-          .catch(done);
-      });
-
-      it('files in directory should be cached', (done) => {
-        const from = 'directory';
-
-        runEmit({
-          expectedAssetKeys: [
-            '.dottedfile',
-            'directoryfile.txt',
-            'nested/deep-nested/deepnested.txt',
-            'nested/nestedfile.txt',
-          ],
-          expectedAssetContent: {
-            '.dottedfile': 'dottedfile contents\nchanged!',
-            'directoryfile.txt': 'newchanged!',
-            'nested/nestedfile.txt': 'changed!',
-          },
-          patterns: [
-            {
-              from,
-              cache: true,
-              transform: function transform(content) {
-                return new Promise((resolve) => {
-                  resolve(`${content}changed!`);
-                });
-              },
-            },
-          ],
-        })
-          .then(() =>
-            cacache.ls(cacheDir).then((cacheEntries) => {
-              const cacheKeys = Object.keys(cacheEntries);
-
-              expect(cacheKeys).toHaveLength(3);
-
-              cacheKeys.forEach((cacheKey) => {
-                // eslint-disable-next-line no-new-func
-                const cacheEntry = new Function(
-                  `'use strict'\nreturn ${cacheKey}`
-                )();
-
-                expect(cacheEntry.pattern.from).toBe(from);
-              });
-            })
-          )
-          .then(done)
-          .catch(done);
-      });
-
-      it('glob should be cached', (done) => {
-        const from = '*.txt';
-
-        runEmit({
-          expectedAssetKeys: ['file.txt'],
-          expectedAssetContent: {
-            'file.txt': 'newchanged!',
-          },
-          patterns: [
-            {
-              from,
-              cache: true,
-              transform: function transform(content) {
-                return new Promise((resolve) => {
-                  resolve(`${content}changed!`);
-                });
-              },
-            },
-          ],
-        })
-          .then(() =>
-            cacache.ls(cacheDir).then((cacheEntries) => {
-              const cacheKeys = Object.keys(cacheEntries);
-
-              expect(cacheKeys).toHaveLength(1);
-
-              cacheKeys.forEach((cacheKey) => {
-                // eslint-disable-next-line no-new-func
-                const cacheEntry = new Function(
-                  `'use strict'\nreturn ${cacheKey}`
-                )();
-
-                expect(cacheEntry.pattern.from).toBe(from);
-              });
-            })
-          )
-          .then(done)
-          .catch(done);
-      });
-
-      it('file should be cached with custom cache key', (done) => {
-        const newContent = 'newchanged!';
-        const from = 'file.txt';
-
-        runEmit({
-          expectedAssetKeys: ['file.txt'],
-          expectedAssetContent: {
-            'file.txt': newContent,
-          },
-          patterns: [
-            {
-              from,
-              cache: {
-                key: 'foobar',
-              },
-              transform(content) {
-                return new Promise((resolve) => {
-                  resolve(`${content}changed!`);
-                });
-              },
-            },
-          ],
-        })
-          .then(() =>
-            cacache.ls(cacheDir).then((cacheEntries) => {
-              const cacheKeys = Object.keys(cacheEntries);
-
-              expect(cacheKeys).toHaveLength(1);
-
-              cacheKeys.forEach((cacheKey) => {
-                expect(cacheKey).toBe('foobar');
-              });
-            })
-          )
-          .then(done)
-          .catch(done);
-      });
-
-      it('binary file should be cached', (done) => {
-        const from = 'file.txt.gz';
-        const content = fs.readFileSync(path.join(HELPER_DIR, from));
-        const expectedNewContent = zlib.gzipSync('newchanged!');
-
-        expect(isGzip(content)).toBe(true);
-        expect(isGzip(expectedNewContent)).toBe(true);
-
-        runEmit({
-          expectedAssetKeys: ['file.txt.gz'],
-          expectedAssetContent: {
-            'file.txt.gz': expectedNewContent,
-          },
-          patterns: [
-            {
-              from,
-              cache: true,
-              // eslint-disable-next-line no-shadow
-              transform: function transform(content) {
-                expect(isGzip(content)).toBe(true);
-
-                return new Promise((resolve) => {
-                  // eslint-disable-next-line no-shadow
-                  zlib.unzip(content, (error, content) => {
-                    if (error) {
-                      throw error;
-                    }
-
-                    const newContent = Buffer.from(`${content}changed!`);
-
-                    // eslint-disable-next-line no-shadow
-                    zlib.gzip(newContent, (error, compressedData) => {
-                      if (error) {
-                        throw error;
-                      }
-
-                      expect(isGzip(compressedData)).toBe(true);
-
-                      return resolve(compressedData);
-                    });
-                  });
-                });
-              },
-            },
-          ],
-        })
-          .then(() =>
-            cacache.ls(cacheDir).then((cacheEntries) => {
-              const cacheKeys = Object.keys(cacheEntries);
-
-              expect(cacheKeys).toHaveLength(1);
-
-              cacheKeys.forEach((cacheKey) => {
-                // eslint-disable-next-line no-new-func
-                const cacheEntry = new Function(
-                  `'use strict'\nreturn ${cacheKey}`
-                )();
-
-                expect(cacheEntry.pattern.from).toBe(from);
-              });
-            })
-          )
-          .then(done)
-          .catch(done);
-      });
-    });
-  });
-
-  it('should move a file and use posix separator for emitting assets', (done) => {
-    runEmit({
-      expectedAssetKeys: ['dir/nestedfile.txt'],
-      patterns: [
-        {
-          context: HELPER_DIR,
-          from: 'directory/nested/nestedfile.txt',
-          to: 'dir',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
 });
diff --git a/test/cache-option.test.js b/test/cache-option.test.js
new file mode 100644
index 0000000..f175961
--- /dev/null
+++ b/test/cache-option.test.js
@@ -0,0 +1,250 @@
+import fs from 'fs';
+import path from 'path';
+import zlib from 'zlib';
+
+import cacache from 'cacache';
+import findCacheDir from 'find-cache-dir';
+import isGzip from 'is-gzip';
+
+import { runEmit } from './utils/run';
+
+const HELPER_DIR = path.join(__dirname, 'helpers');
+
+describe('cache option', () => {
+  const cacheDir = findCacheDir({ name: 'copy-webpack-plugin' });
+
+  beforeEach(() => cacache.rm.all(cacheDir));
+
+  it('should cache when "from" is a file', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
+
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': newContent,
+      },
+      patterns: [
+        {
+          from,
+          cache: true,
+          transform: function transform(content) {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+
+          cacheKeys.forEach((cacheKey) => {
+            // eslint-disable-next-line no-new-func
+            const cacheEntry = new Function(
+              `'use strict'\nreturn ${cacheKey}`
+            )();
+
+            expect(cacheEntry.pattern.from).toBe(from);
+          });
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache files when "from" is a directory', (done) => {
+    const from = 'directory';
+
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      expectedAssetContent: {
+        '.dottedfile': 'dottedfile contents\nchanged!',
+        'directoryfile.txt': 'newchanged!',
+        'nested/nestedfile.txt': 'changed!',
+      },
+      patterns: [
+        {
+          from,
+          cache: true,
+          transform: function transform(content) {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(3);
+
+          cacheKeys.forEach((cacheKey) => {
+            // eslint-disable-next-line no-new-func
+            const cacheEntry = new Function(
+              `'use strict'\nreturn ${cacheKey}`
+            )();
+
+            expect(cacheEntry.pattern.from).toBe(from);
+          });
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache when "from" is a glob', (done) => {
+    const from = 'directory/*.txt';
+
+    runEmit({
+      expectedAssetKeys: ['directory/directoryfile.txt'],
+      expectedAssetContent: {
+        'directory/directoryfile.txt': 'newchanged!',
+      },
+      patterns: [
+        {
+          from,
+          cache: true,
+          transform: function transform(content) {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+
+          cacheKeys.forEach((cacheKey) => {
+            // eslint-disable-next-line no-new-func
+            const cacheEntry = new Function(
+              `'use strict'\nreturn ${cacheKey}`
+            )();
+
+            // Todo need investigate
+            expect(cacheEntry.pattern.from.replace(/\\/, '/')).toBe(from);
+          });
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache file with custom cache key', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
+
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': newContent,
+      },
+      patterns: [
+        {
+          from,
+          cache: {
+            key: 'foobar',
+          },
+          transform(content) {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+
+          cacheKeys.forEach((cacheKey) => {
+            expect(cacheKey).toBe('foobar');
+          });
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache binary file', (done) => {
+    const from = 'file.txt.gz';
+    const content = fs.readFileSync(path.join(HELPER_DIR, from));
+    const expectedNewContent = zlib.gzipSync('newchanged!');
+
+    expect(isGzip(content)).toBe(true);
+    expect(isGzip(expectedNewContent)).toBe(true);
+
+    runEmit({
+      expectedAssetKeys: ['file.txt.gz'],
+      expectedAssetContent: {
+        'file.txt.gz': expectedNewContent,
+      },
+      patterns: [
+        {
+          from,
+          cache: true,
+          // eslint-disable-next-line no-shadow
+          transform: function transform(content) {
+            expect(isGzip(content)).toBe(true);
+
+            return new Promise((resolve) => {
+              // eslint-disable-next-line no-shadow
+              zlib.unzip(content, (error, content) => {
+                if (error) {
+                  throw error;
+                }
+
+                const newContent = Buffer.from(`${content}changed!`);
+
+                // eslint-disable-next-line no-shadow
+                zlib.gzip(newContent, (error, compressedData) => {
+                  if (error) {
+                    throw error;
+                  }
+
+                  expect(isGzip(compressedData)).toBe(true);
+
+                  return resolve(compressedData);
+                });
+              });
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+
+          cacheKeys.forEach((cacheKey) => {
+            // eslint-disable-next-line no-new-func
+            const cacheEntry = new Function(
+              `'use strict'\nreturn ${cacheKey}`
+            )();
+
+            expect(cacheEntry.pattern.from).toBe(from);
+          });
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/context-option.test.js b/test/context-option.test.js
new file mode 100644
index 0000000..a42eaf9
--- /dev/null
+++ b/test/context-option.test.js
@@ -0,0 +1,269 @@
+import path from 'path';
+
+import { runEmit } from './utils/run';
+
+const HELPER_DIR = path.join(__dirname, 'helpers');
+
+describe('context option', () => {
+  it('should work when "from" is a file', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directoryfile.txt'],
+      patterns: [
+        {
+          from: 'directoryfile.txt',
+          context: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a file and "context" with special characters', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directoryfile.txt'],
+      patterns: [
+        {
+          from: 'directoryfile.txt',
+          context: '[special?directory]',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: ['deep-nested/deepnested.txt', 'nestedfile.txt'],
+      patterns: [
+        {
+          from: 'nested',
+          context: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a directory and "to" is a new directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'newdirectory/deep-nested/deepnested.txt',
+        'newdirectory/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          context: 'directory',
+          from: 'nested',
+          to: 'newdirectory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a directory and "context" with special characters', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directoryfile.txt',
+        '(special-*file).txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          // Todo strange behavour when you use `HELPER_DIR`, need investigate for next major release
+          from: '.',
+          context: '[special?directory]',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'nested/**/*',
+          context: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a glob and "to" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'nested/directoryfile.txt',
+        'nested/nested/deep-nested/deepnested.txt',
+        'nested/nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          context: 'directory',
+          from: '**/*',
+          to: 'nested',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a glob and "to" is a directory and "content" is an absolute path', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'nested/directoryfile.txt',
+        'nested/nested/deep-nested/deepnested.txt',
+        'nested/nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          context: path.join(HELPER_DIR, 'directory'),
+          from: '**/*',
+          to: 'nested',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a glob and "context" with special characters', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directoryfile.txt',
+        '(special-*file).txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: '**/*',
+          context: '[special?directory]',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a glob and "context" with special characters #2', (done) => {
+    runEmit({
+      expectedAssetKeys: ['(special-*file).txt'],
+      patterns: [
+        {
+          from: '(special-*file).txt',
+          context: '[special?directory]',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should work when "from" is a file and "context" is an absolute path', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directoryfile.txt'],
+      patterns: [
+        {
+          from: 'directoryfile.txt',
+          context: path.join(HELPER_DIR, 'directory'),
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should override webpack config context with an absolute path', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'newdirectory/deep-nested/deepnested.txt',
+        'newdirectory/nestedfile.txt',
+      ],
+      options: {
+        context: path.join(HELPER_DIR, 'directory'),
+      },
+      patterns: [
+        {
+          from: 'nested',
+          to: 'newdirectory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should override webpack config context with a relative path', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'newdirectory/deep-nested/deepnested.txt',
+        'newdirectory/nestedfile.txt',
+      ],
+      options: {
+        context: 'directory',
+      },
+      patterns: [
+        {
+          from: 'nested',
+          to: 'newdirectory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should override global context on pattern context with a relative path', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'newdirectory/deep-nested/deepnested.txt',
+        'newdirectory/nestedfile.txt',
+      ],
+      options: {
+        context: 'directory',
+      },
+      patterns: [
+        {
+          context: 'nested',
+          from: '.',
+          to: 'newdirectory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('overrides webpack config context with an absolute path', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'newdirectory/file.txt',
+        'newdirectory/nesteddir/deepnesteddir/deepnesteddir.txt',
+        'newdirectory/nesteddir/nestedfile.txt',
+      ],
+      options: {
+        context: path.join(HELPER_DIR, 'dir (86)'),
+      },
+      patterns: [
+        {
+          from: '**/*',
+          to: 'newdirectory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/flatten-option.test.js b/test/flatten-option.test.js
new file mode 100644
index 0000000..80766ed
--- /dev/null
+++ b/test/flatten-option.test.js
@@ -0,0 +1,147 @@
+import { runEmit } from './utils/run';
+
+describe('flatten option', () => {
+  it('should flatten a directory\'s files to a root directory when "from" is a file', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directoryfile.txt'],
+      patterns: [
+        {
+          flatten: true,
+          from: 'directory/directoryfile.txt',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten a directory\'s files to a new directory when "from" is a file', (done) => {
+    runEmit({
+      expectedAssetKeys: ['nested/directoryfile.txt'],
+      patterns: [
+        {
+          flatten: true,
+          from: 'directory/directoryfile.txt',
+          to: 'nested',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten a directory\'s files to a root directory when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'deepnested.txt',
+        'directoryfile.txt',
+        'nestedfile.txt',
+      ],
+      patterns: [
+        {
+          flatten: true,
+          from: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten a directory\'s files to new directory when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'newdirectory/.dottedfile',
+        'newdirectory/deepnested.txt',
+        'newdirectory/directoryfile.txt',
+        'newdirectory/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          flatten: true,
+          from: 'directory',
+          to: 'newdirectory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten a directory\'s files to a root directory when "from" is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'deepnested.txt',
+        'directoryfile.txt',
+        'nestedfile.txt',
+      ],
+      patterns: [
+        {
+          flatten: true,
+          from: 'directory/**/*',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten a directory\'s files to a new directory when "from" is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'nested/deepnested.txt',
+        'nested/directoryfile.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          flatten: true,
+          from: 'directory/**/*',
+          to: 'nested',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten files in a relative context to a root directory when "from" is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'deepnested.txt',
+        'directoryfile.txt',
+        'nestedfile.txt',
+      ],
+      patterns: [
+        {
+          context: 'directory',
+          flatten: true,
+          from: '**/*',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should flatten files in a relative context to a non-root directory when "from" is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'nested/deepnested.txt',
+        'nested/directoryfile.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          context: 'directory',
+          flatten: true,
+          from: '**/*',
+          to: 'nested',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/force-option.test.js b/test/force-option.test.js
new file mode 100644
index 0000000..1567007
--- /dev/null
+++ b/test/force-option.test.js
@@ -0,0 +1,237 @@
+import { runForce } from './utils/run';
+
+describe('force option', () => {
+  describe('is not specified', () => {
+    it('should not overwrite a file already in the compilation by default when "from" is a file', (done) => {
+      runForce({
+        existingAssets: ['file.txt'],
+        expectedAssetKeys: ['file.txt'],
+        expectedAssetContent: {
+          'file.txt': 'existing',
+        },
+        patterns: [
+          {
+            from: 'file.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should not overwrite files already in the compilation when "from" is a directory', (done) => {
+      runForce({
+        existingAssets: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        expectedAssetContent: {
+          '.dottedfile': 'existing',
+          'nested/deep-nested/deepnested.txt': 'existing',
+          'nested/nestedfile.txt': 'existing',
+          'directoryfile.txt': 'existing',
+        },
+        patterns: [
+          {
+            from: 'directory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should not overwrite files already in the compilation when "from" is a glob', (done) => {
+      runForce({
+        existingAssets: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        expectedAssetKeys: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        expectedAssetContent: {
+          'directory/nested/deep-nested/deepnested.txt': 'existing',
+          'directory/nested/nestedfile.txt': 'existing',
+          'directory/directoryfile.txt': 'existing',
+        },
+        patterns: [
+          {
+            from: 'directory/**/*',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is "false" (Boolean)', () => {
+    it('should not overwrite a file already in the compilation by default when "from" is a file', (done) => {
+      runForce({
+        existingAssets: ['file.txt'],
+        expectedAssetKeys: ['file.txt'],
+        expectedAssetContent: {
+          'file.txt': 'existing',
+        },
+        patterns: [
+          {
+            force: false,
+            from: 'file.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should not overwrite files already in the compilation when "from" is a directory', (done) => {
+      runForce({
+        existingAssets: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        expectedAssetContent: {
+          '.dottedfile': 'existing',
+          'nested/deep-nested/deepnested.txt': 'existing',
+          'nested/nestedfile.txt': 'existing',
+          'directoryfile.txt': 'existing',
+        },
+        patterns: [
+          {
+            force: false,
+            from: 'directory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should not overwrite files already in the compilation when "from" is a glob', (done) => {
+      runForce({
+        existingAssets: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        expectedAssetKeys: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        expectedAssetContent: {
+          'directory/nested/deep-nested/deepnested.txt': 'existing',
+          'directory/nested/nestedfile.txt': 'existing',
+          'directory/directoryfile.txt': 'existing',
+        },
+        patterns: [
+          {
+            force: false,
+            from: 'directory/**/*',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is "true" (Boolean)', () => {
+    it('should force overwrite a file already in the compilation when "from" is a file', (done) => {
+      runForce({
+        existingAssets: ['file.txt'],
+        expectedAssetKeys: ['file.txt'],
+        expectedAssetContent: {
+          'file.txt': 'new',
+        },
+        patterns: [
+          {
+            force: true,
+            from: 'file.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should force overwrite files already in the compilation when "from" is a directory', (done) => {
+      runForce({
+        existingAssets: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        expectedAssetContent: {
+          '.dottedfile': 'dottedfile contents\n',
+          'nested/deep-nested/deepnested.txt': '',
+          'nested/nestedfile.txt': '',
+          'directoryfile.txt': 'new',
+        },
+        patterns: [
+          {
+            force: true,
+            from: 'directory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should force overwrite files already in the compilation when "from" is a glob', (done) => {
+      runForce({
+        existingAssets: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        expectedAssetKeys: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        expectedAssetContent: {
+          'directory/nested/deep-nested/deepnested.txt': '',
+          'directory/nested/nestedfile.txt': '',
+          'directory/directoryfile.txt': 'new',
+        },
+        patterns: [
+          {
+            force: true,
+            from: 'directory/**/*',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+});
diff --git a/test/from-option.test.js b/test/from-option.test.js
new file mode 100644
index 0000000..8a9662d
--- /dev/null
+++ b/test/from-option.test.js
@@ -0,0 +1,522 @@
+import path from 'path';
+
+import { runEmit } from './utils/run';
+
+const HELPER_DIR = path.join(__dirname, 'helpers');
+
+describe('from option', () => {
+  describe('is a file', () => {
+    it('should move a file', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file when "from" an absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'file.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file from nesting directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directoryfile.txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file from nesting directory when "from" an absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directoryfile.txt'],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'directory/directoryfile.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file (symbolic link)', (done) => {
+      runEmit({
+        symlink: true,
+        expectedAssetKeys: process.platform === 'win32' ? [] : ['file-ln.txt'],
+        patterns: [
+          {
+            from: 'symlink/file-ln.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should warn when file not found', (done) => {
+      runEmit({
+        expectedAssetKeys: [],
+        expectedWarnings: [
+          new Error(
+            `unable to locate 'nonexistent.txt' at '${HELPER_DIR}${path.sep}nonexistent.txt'`
+          ),
+        ],
+        patterns: [
+          {
+            from: 'nonexistent.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is a directory', () => {
+    it('should move files', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files when "from" is current directory', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.file.txt',
+          '[!]/hello.txt',
+          '[special?directory]/(special-*file).txt',
+          '[special?directory]/directoryfile.txt',
+          '[special?directory]/nested/nestedfile.txt',
+          'binextension.bin',
+          'dir (86)/file.txt',
+          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
+          'dir (86)/nesteddir/nestedfile.txt',
+          'directory/.dottedfile',
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+          'file.txt',
+          'file.txt.gz',
+          'noextension',
+        ],
+        patterns: [
+          {
+            from: '.',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files when "from" is relative path to context', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.file.txt',
+          '[!]/hello.txt',
+          '[special?directory]/(special-*file).txt',
+          '[special?directory]/directoryfile.txt',
+          '[special?directory]/nested/nestedfile.txt',
+          'binextension.bin',
+          'dir (86)/file.txt',
+          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
+          'dir (86)/nesteddir/nestedfile.txt',
+          'directory/.dottedfile',
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+          'file.txt',
+          'file.txt.gz',
+          'noextension',
+        ],
+        patterns: [
+          {
+            from: '../helpers',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files with a forward slash', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory/',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files from symbolic link', (done) => {
+      runEmit({
+        // Windows doesn't support symbolic link
+        symlink: true,
+        expectedAssetKeys:
+          process.platform === 'win32'
+            ? []
+            : ['file.txt', 'nested-directory/file-in-nested-directory.txt'],
+        patterns: [
+          {
+            from: 'symlink/directory-ln',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it("should move files when 'from' is a absolute path", (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'directory'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it("should move files when 'from' with special characters", (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'directoryfile.txt',
+          '(special-*file).txt',
+          'nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from:
+              path.sep === '/' ? '[special?directory]' : '[specialdirectory]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files from nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['deep-nested/deepnested.txt', 'nestedfile.txt'],
+        patterns: [
+          {
+            from: 'directory/nested',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files from nested directory with an absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['deep-nested/deepnested.txt', 'nestedfile.txt'],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'directory/nested'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should warn when directory not found', (done) => {
+      runEmit({
+        expectedAssetKeys: [],
+        expectedWarnings: [
+          new Error(
+            `unable to locate 'nonexistent' at '${HELPER_DIR}${path.sep}nonexistent'`
+          ),
+        ],
+        patterns: [
+          {
+            from: 'nonexistent',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is a glob', () => {
+    it('should move files', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: '*.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files when a glob contains absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, '*.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files using globstar', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '[!]/hello.txt',
+          'binextension.bin',
+          'dir (86)/file.txt',
+          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
+          'dir (86)/nesteddir/nestedfile.txt',
+          'file.txt',
+          'file.txt.gz',
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+          '[special?directory]/directoryfile.txt',
+          '[special?directory]/(special-*file).txt',
+          '[special?directory]/nested/nestedfile.txt',
+          'noextension',
+        ],
+        patterns: [
+          {
+            from: '**/*',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files using globstar and contains an absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '[!]/hello.txt',
+          'file.txt',
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+          '[special?directory]/directoryfile.txt',
+          '[special?directory]/(special-*file).txt',
+          '[special?directory]/nested/nestedfile.txt',
+          'dir (86)/file.txt',
+          'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt',
+          'dir (86)/nesteddir/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, '**/*.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files in nested directory using globstar', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'nested/[!]/hello-d41d8c.txt',
+          'nested/binextension-d41d8c.bin',
+          'nested/dir (86)/file-d41d8c.txt',
+          'nested/dir (86)/nesteddir/deepnesteddir/deepnesteddir-d41d8c.txt',
+          'nested/dir (86)/nesteddir/nestedfile-d41d8c.txt',
+          'nested/file-22af64.txt',
+          'nested/file.txt-5b311c.gz',
+          'nested/directory/directoryfile-22af64.txt',
+          'nested/directory/nested/deep-nested/deepnested-d41d8c.txt',
+          'nested/directory/nested/nestedfile-d41d8c.txt',
+          'nested/[special?directory]/(special-*file)-0bd650.txt',
+          'nested/[special?directory]/directoryfile-22af64.txt',
+          'nested/[special?directory]/nested/nestedfile-d41d8c.txt',
+          'nested/noextension-d41d8c',
+        ],
+        patterns: [
+          {
+            from: '**/*',
+            to: 'nested/[path][name]-[hash:6].[ext]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files from nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directory/directoryfile.txt'],
+        patterns: [
+          {
+            from: 'directory/directory*.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files from nested directory #2', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory/**/*.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files using bracketed glob', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'directory/directoryfile.txt',
+          'directory/nested/deep-nested/deepnested.txt',
+          'directory/nested/nestedfile.txt',
+          'file.txt',
+          'noextension',
+        ],
+        patterns: [
+          {
+            from: '{file.txt,noextension,directory/**/*}',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files (symbolic link)', (done) => {
+      runEmit({
+        // Windows doesn't support symbolic link
+        symlink: true,
+        expectedAssetKeys:
+          process.platform === 'win32'
+            ? []
+            : [
+                'symlink/directory-ln/file.txt',
+                'symlink/directory-ln/nested-directory/file-in-nested-directory.txt',
+                'symlink/directory/file.txt',
+                'symlink/directory/nested-directory/file-in-nested-directory.txt',
+                'symlink/file-ln.txt',
+                'symlink/file.txt',
+              ],
+        patterns: [
+          {
+            from: 'symlink/**/*.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is a object with glob and glob options', () => {
+    it('should move files', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: {
+              glob: '*.txt',
+            },
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files exclude dot files', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: {
+              glob: '*.txt',
+              dot: false,
+            },
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files with dot files', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt', '.file.txt'],
+        patterns: [
+          {
+            from: {
+              glob: '*.txt',
+              dot: true,
+            },
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+});
diff --git a/test/globOptions-option.test.js b/test/globOptions-option.test.js
new file mode 100644
index 0000000..a6dc0a3
--- /dev/null
+++ b/test/globOptions-option.test.js
@@ -0,0 +1,35 @@
+import { runEmit } from './utils/run';
+
+describe('from option', () => {
+  it('should move files exclude dot files', (done) => {
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      patterns: [
+        {
+          from: '*.txt',
+          globOptions: {
+            dot: false,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move files include dot files', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.file.txt', 'file.txt'],
+      patterns: [
+        {
+          from: '*.txt',
+          globOptions: {
+            dot: true,
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/helpers/watch/.gitkeep b/test/helpers/watch/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/watch/directory/.gitkeep b/test/helpers/watch/directory/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test/ignore-option.test.js b/test/ignore-option.test.js
new file mode 100644
index 0000000..50db76c
--- /dev/null
+++ b/test/ignore-option.test.js
@@ -0,0 +1,186 @@
+import { runEmit } from './utils/run';
+
+describe('ignore option', () => {
+  it('should ignore files when "from" is a file', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      patterns: [
+        {
+          ignore: ['file.*'],
+          from: 'file.txt',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+      ],
+      patterns: [
+        {
+          ignore: ['*/nestedfile.*'],
+          from: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files in nested directory when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.dottedfile', 'directoryfile.txt'],
+      patterns: [
+        {
+          ignore: ['**/nested/**'],
+          from: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files when from is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directory/directoryfile.txt',
+        'directory/nested/deep-nested/deepnested.txt',
+      ],
+      patterns: [
+        {
+          ignore: ['*nestedfile.*'],
+          from: 'directory/**/*',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files in nested directory when from is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directory/directoryfile.txt'],
+      patterns: [
+        {
+          ignore: ['*/nested/**'],
+          from: 'directory/**/*',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files with a certain extension', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.dottedfile'],
+      patterns: [
+        {
+          ignore: ['*.txt'],
+          from: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files with multiple ignore patterns', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directory/nested/nestedfile.txt'],
+      patterns: [
+        {
+          ignore: ['directoryfile.*', '**/deep-nested/**'],
+          from: 'directory/**/*',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files except those with dots', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.dottedfile'],
+      options: {
+        ignore: [
+          {
+            dot: false,
+            glob: '**/*',
+          },
+        ],
+      },
+      patterns: [
+        {
+          from: 'directory',
+          ignore: [
+            {
+              dot: false,
+              glob: '**/*',
+            },
+          ],
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files that start with a dot', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          ignore: ['.dottedfile'],
+          from: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignores all files even if they start with a dot', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      options: {
+        ignore: ['**/*'],
+      },
+      patterns: [
+        {
+          from: 'directory',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files when "from" is a file (global ignore)', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      options: {
+        ignore: ['file.*'],
+      },
+      patterns: [
+        {
+          ignore: ['file.*'],
+          from: 'file.txt',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/test-option.test.js b/test/test-option.test.js
new file mode 100644
index 0000000..c050316
--- /dev/null
+++ b/test/test-option.test.js
@@ -0,0 +1,73 @@
+import path from 'path';
+
+import { runEmit } from './utils/run';
+
+describe('test option', () => {
+  it('should move files to a root directory with [1]', (done) => {
+    runEmit({
+      expectedAssetKeys: ['txt'],
+      patterns: [
+        {
+          from: 'directory/nested/deep-nested',
+          to: '[1]',
+          test: /\.([^.]*)$/,
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move files to a non-root directory with [1]', (done) => {
+    runEmit({
+      expectedAssetKeys: ['nested/txt'],
+      patterns: [
+        {
+          from: 'directory/nested/deep-nested',
+          to: 'nested/[1]',
+          test: /\.([^.]*)$/,
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move files and flatten them to a non-root directory with [1]-[2].[ext]', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'nested/deep-nested-deepnested.txt',
+        'nested/directory-directoryfile.txt',
+        'nested/nested-nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory/**/*',
+          test: `([^\\${path.sep}]+)\\${path.sep}([^\\${path.sep}]+)\\.\\w+$`,
+          to: 'nested/[1]-[2].[ext]',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move files and flatten them to a root directory with [1]-[2].[ext]', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'deep-nested-deepnested.txt',
+        'directory-directoryfile.txt',
+        'nested-nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory/**/*',
+          test: `([^\\${path.sep}]+)\\${path.sep}([^\\${path.sep}]+)\\.\\w+$`,
+          to: '[1]-[2].[ext]',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/to-option.test.js b/test/to-option.test.js
new file mode 100644
index 0000000..6245c5e
--- /dev/null
+++ b/test/to-option.test.js
@@ -0,0 +1,452 @@
+import path from 'path';
+
+import { runEmit } from './utils/run';
+
+const BUILD_DIR = path.join(__dirname, 'build');
+const TEMP_DIR = path.join(__dirname, 'tempdir');
+const HELPER_DIR = path.join(__dirname, 'helpers');
+
+describe('to option', () => {
+  describe('is a file', () => {
+    it('should move a file to a new file', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newfile.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: 'newfile.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new file when "to" is absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['../tempdir/newfile.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: path.join(TEMP_DIR, 'newfile.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new file inside nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/newfile.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: 'newdirectory/newfile.txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new file inside nested directory when "to" an absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/newfile.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: path.join(BUILD_DIR, 'newdirectory/newfile.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new file inside other directory what out of context', (done) => {
+      runEmit({
+        expectedAssetKeys: ['../tempdir/newdirectory/newfile.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: path.join(TEMP_DIR, 'newdirectory/newfile.txt'),
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file using invalid template syntax', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directory/[md5::base64:20].txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+            to: 'directory/[md5::base64:20].txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is a directory', () => {
+    it('should move a file to a new directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: 'newdirectory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory out of context', (done) => {
+      runEmit({
+        expectedAssetKeys: ['../tempdir/file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: TEMP_DIR,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory with a forward slash', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: 'newdirectory/',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory with an extension and path separator at end', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory.ext/file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: `newdirectory.ext${path.sep}`,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory when "to" is absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: BUILD_DIR,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory when "to" is absolute path with a forward slash', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: `${BUILD_DIR}/`,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory from nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/directoryfile.txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+            to: 'newdirectory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory from nested directory when "from" is absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/directoryfile.txt'],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'directory', 'directoryfile.txt'),
+            to: 'newdirectory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file to a new directory from nested directory when "from" is absolute path with a forward slash', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/directoryfile.txt'],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'directory', 'directoryfile.txt'),
+            to: 'newdirectory/',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files to a new directory', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'newdirectory/.dottedfile',
+          'newdirectory/directoryfile.txt',
+          'newdirectory/nested/deep-nested/deepnested.txt',
+          'newdirectory/nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory',
+            to: 'newdirectory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files to a new nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'newdirectory/deep-nested/deepnested.txt',
+          'newdirectory/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: path.join(HELPER_DIR, 'directory', 'nested'),
+            to: 'newdirectory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files to a new directory out of context', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '../tempdir/.dottedfile',
+          '../tempdir/directoryfile.txt',
+          '../tempdir/nested/deep-nested/deepnested.txt',
+          '../tempdir/nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory',
+            to: TEMP_DIR,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files to a new directory when "to" is absolute path', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory',
+            to: BUILD_DIR,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files to a new directory when "to" is absolute path with a forward slash', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          '.dottedfile',
+          'directoryfile.txt',
+          'nested/deep-nested/deepnested.txt',
+          'nested/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory',
+            to: `${BUILD_DIR}/`,
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files to a new directory from nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'newdirectory/deep-nested/deepnested.txt',
+          'newdirectory/nestedfile.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory/nested',
+            to: 'newdirectory',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
+  describe('is a template', () => {
+    it('should move a file using "contenthash"', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directory/22af64.txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+            to: 'directory/[contenthash:6].txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file using custom `contenthash` digest', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directory/c2a6.txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+            to: 'directory/[sha1:contenthash:hex:4].txt',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file using "name" and "ext"', (done) => {
+      runEmit({
+        expectedAssetKeys: ['binextension.bin'],
+        patterns: [
+          {
+            from: 'binextension.bin',
+            to: '[name].[ext]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file using "name", "contenthash" and "ext"', (done) => {
+      runEmit({
+        expectedAssetKeys: ['file-22af64.txt'],
+        patterns: [
+          {
+            from: 'file.txt',
+            to: '[name]-[contenthash:6].[ext]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file from nested directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['directoryfile-22af64.txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+            to: '[name]-[hash:6].[ext]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file from nested directory to new directory', (done) => {
+      runEmit({
+        expectedAssetKeys: ['newdirectory/directoryfile-22af64.txt'],
+        patterns: [
+          {
+            from: 'directory/directoryfile.txt',
+            to: 'newdirectory/[name]-[hash:6].[ext]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move a file without an extension using "name", "ext"', (done) => {
+      runEmit({
+        expectedAssetKeys: ['noextension.d41d8c.newext'],
+        patterns: [
+          {
+            from: 'noextension',
+            to: '[name][ext].[contenthash:6].newext',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('should move files using "path", "name", "contenthash" and "ext"', (done) => {
+      runEmit({
+        expectedAssetKeys: [
+          'newdirectory/.dottedfile-79d39f',
+          'newdirectory/directoryfile-22af64.txt',
+          'newdirectory/nested/deep-nested/deepnested-d41d8c.txt',
+          'newdirectory/nested/nestedfile-d41d8c.txt',
+        ],
+        patterns: [
+          {
+            from: 'directory',
+            to: 'newdirectory/[path][name]-[contenthash:6].[ext]',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+});
diff --git a/test/toType-option.test.js b/test/toType-option.test.js
new file mode 100644
index 0000000..d6490f5
--- /dev/null
+++ b/test/toType-option.test.js
@@ -0,0 +1,107 @@
+import path from 'path';
+
+import { runEmit } from './utils/run';
+import { MockCompilerNoStat } from './utils/mocks';
+
+const HELPER_DIR = path.join(__dirname, 'helpers');
+
+describe('toType option', () => {
+  it('should move a file to a new file', (done) => {
+    runEmit({
+      expectedAssetKeys: ['new-file.txt'],
+      patterns: [
+        {
+          from: 'file.txt',
+          to: 'new-file.txt',
+          toType: 'file',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move a file to a new directory', (done) => {
+    runEmit({
+      expectedAssetKeys: ['new-file.txt/file.txt'],
+      patterns: [
+        {
+          from: 'file.txt',
+          to: 'new-file.txt',
+          toType: 'dir',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move a file to a new directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directory/directorynew-directoryfile.txt.22af645d1859cb5ca6da0c484f1f37ea.22af645d1859cb5ca6da0c484f1f37ea.22af645d.22af645d.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory/directoryfile.*',
+          to:
+            '[path][folder]new-[name].[ext].[hash].[contenthash].[md5:contenthash:hex:8].[md5:hash:hex:8].txt',
+          toType: 'template',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move a file to a new file with no extension', (done) => {
+    runEmit({
+      expectedAssetKeys: ['newname'],
+      patterns: [
+        {
+          from: 'file.txt',
+          to: 'newname',
+          toType: 'file',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should move a file to a new directory with an extension', (done) => {
+    runEmit({
+      expectedAssetKeys: ['newdirectory.ext/file.txt'],
+      patterns: [
+        {
+          from: 'file.txt',
+          to: 'newdirectory.ext',
+          toType: 'dir',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should warn when file not found and stats is undefined', (done) => {
+    runEmit({
+      compiler: new MockCompilerNoStat(),
+      expectedAssetKeys: [],
+      expectedWarnings: [
+        new Error(
+          `unable to locate 'nonexistent.txt' at '${HELPER_DIR}${path.sep}nonexistent.txt'`
+        ),
+      ],
+      patterns: [
+        {
+          from: 'nonexistent.txt',
+          to: '.',
+          toType: 'dir',
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/transform-option.test.js b/test/transform-option.test.js
new file mode 100644
index 0000000..a118d9c
--- /dev/null
+++ b/test/transform-option.test.js
@@ -0,0 +1,184 @@
+import path from 'path';
+
+import { runEmit } from './utils/run';
+
+const HELPER_DIR = path.join(__dirname, 'helpers');
+
+describe('transform option', () => {
+  it('should transform file when "from" is a file', (done) => {
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': 'newchanged',
+      },
+      patterns: [
+        {
+          from: 'file.txt',
+          transform(content, absoluteFrom) {
+            expect(absoluteFrom.includes(HELPER_DIR)).toBe(true);
+
+            return `${content}changed`;
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should transform target path of every when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      expectedAssetContent: {
+        '.dottedfile': 'dottedfile contents\nchanged',
+        'directoryfile.txt': 'newchanged',
+        'nested/deep-nested/deepnested.txt': 'changed',
+        'nested/nestedfile.txt': 'changed',
+      },
+      patterns: [
+        {
+          from: 'directory',
+          transform(content, absoluteFrom) {
+            expect(absoluteFrom.includes(HELPER_DIR)).toBe(true);
+
+            return `${content}changed`;
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should transform target path of every file when "from" is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directory/directoryfile.txt',
+        'directory/nested/deep-nested/deepnested.txt',
+        'directory/nested/nestedfile.txt',
+      ],
+      expectedAssetContent: {
+        'directory/directoryfile.txt': 'newchanged',
+        'directory/nested/deep-nested/deepnested.txt': 'changed',
+        'directory/nested/nestedfile.txt': 'changed',
+      },
+      patterns: [
+        {
+          from: 'directory/**/*',
+          transform(content, absoluteFrom) {
+            expect(absoluteFrom.includes(HELPER_DIR)).toBe(true);
+
+            return `${content}changed`;
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should transform file when function return Promise', (done) => {
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': 'newchanged!',
+      },
+      patterns: [
+        {
+          from: 'file.txt',
+          transform(content) {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should transform target path when async function used', (done) => {
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': 'newchanged!',
+      },
+      patterns: [
+        {
+          from: 'file.txt',
+          async transform(content) {
+            const newPath = await new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+
+            return newPath;
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should warn when function throw error', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      expectedErrors: [new Error('a failure happened')],
+      patterns: [
+        {
+          from: 'file.txt',
+          transform() {
+            // eslint-disable-next-line no-throw-literal
+            throw new Error('a failure happened');
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should warn when Promise was rejected', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      expectedErrors: [new Error('a failure happened')],
+      patterns: [
+        {
+          from: 'file.txt',
+          transform() {
+            return new Promise((resolve, reject) => {
+              return reject(new Error('a failure happened'));
+            });
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should warn when async function throw error', (done) => {
+    runEmit({
+      expectedAssetKeys: [],
+      expectedErrors: [new Error('a failure happened')],
+      patterns: [
+        {
+          from: 'file.txt',
+          async transform() {
+            await new Promise((resolve, reject) => {
+              reject(new Error('a failure happened'));
+            });
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/transformPath-option.test.js b/test/transformPath-option.test.js
index 3ab1b16..d48c6f2 100644
--- a/test/transformPath-option.test.js
+++ b/test/transformPath-option.test.js
@@ -51,20 +51,13 @@ describe('transformPath option', () => {
   it('should transform target path of every file when "from" is a glob', (done) => {
     runEmit({
       expectedAssetKeys: [
-        '/some/path/(special-*file).txt.tst',
-        '/some/path/binextension.bin.tst',
         '/some/path/deepnested.txt.tst',
-        '/some/path/deepnesteddir.txt.tst',
-        '/some/path/file.txt.tst',
-        '/some/path/file.txt.gz.tst',
         '/some/path/directoryfile.txt.tst',
         '/some/path/nestedfile.txt.tst',
-        '/some/path/noextension.tst',
-        '/some/path/hello.txt.tst',
       ],
       patterns: [
         {
-          from: '**/*',
+          from: 'directory/**/*',
           transformPath(targetPath, absoluteFrom) {
             expect(absoluteFrom.includes(HELPER_DIR)).toBe(true);
 
@@ -119,7 +112,7 @@ describe('transformPath option', () => {
       .catch(done);
   });
 
-  it('should warns when function throw error', (done) => {
+  it('should warn when function throw error', (done) => {
     runEmit({
       expectedAssetKeys: [],
       expectedErrors: [new Error('a failure happened')],
@@ -136,7 +129,7 @@ describe('transformPath option', () => {
       .catch(done);
   });
 
-  it('should warns when Promise was rejected', (done) => {
+  it('should warn when Promise was rejected', (done) => {
     runEmit({
       expectedAssetKeys: [],
       expectedErrors: [new Error('a failure happened')],
@@ -155,7 +148,7 @@ describe('transformPath option', () => {
       .catch(done);
   });
 
-  it('should warns when async function throw error', (done) => {
+  it('should warn when async function throw error', (done) => {
     runEmit({
       expectedAssetKeys: [],
       expectedErrors: [new Error('a failure happened')],
@@ -177,24 +170,13 @@ describe('transformPath option', () => {
   it('should transform target path of every file in glob after applying template', (done) => {
     runEmit({
       expectedAssetKeys: [
-        'transformed/[!]/hello-d41d8c.txt',
-        'transformed/[special?directory]/directoryfile-22af64.txt',
-        'transformed/[special?directory]/(special-*file)-0bd650.txt',
-        'transformed/[special?directory]/nested/nestedfile-d41d8c.txt',
-        'transformed/binextension-d41d8c.bin',
-        'transformed/dir (86)/file-d41d8c.txt',
-        'transformed/dir (86)/nesteddir/deepnesteddir/deepnesteddir-d41d8c.txt',
-        'transformed/dir (86)/nesteddir/nestedfile-d41d8c.txt',
-        'transformed/file-22af64.txt',
-        'transformed/file.txt-5b311c.gz',
         'transformed/directory/directoryfile-22af64.txt',
         'transformed/directory/nested/deep-nested/deepnested-d41d8c.txt',
         'transformed/directory/nested/nestedfile-d41d8c.txt',
-        'transformed/noextension-d41d8c',
       ],
       patterns: [
         {
-          from: '**/*',
+          from: 'directory/**/*',
           to: 'nested/[path][name]-[hash:6].[ext]',
           transformPath(targetPath, absoluteFrom) {
             expect(absoluteFrom.includes(HELPER_DIR)).toBe(true);
diff --git a/test/utils/run.js b/test/utils/run.js
index 17cf14f..26efbbf 100644
--- a/test/utils/run.js
+++ b/test/utils/run.js
@@ -34,7 +34,12 @@ function run(opts) {
         opts.options.ignore = [];
       }
 
-      opts.options.ignore.push('symlink/**/*', 'file-ln.txt', 'directory-ln');
+      opts.options.ignore.push(
+        'symlink/**/*',
+        'file-ln.txt',
+        'directory-ln',
+        'watch/**/*'
+      );
     }
 
     new CopyPlugin(opts.patterns, opts.options).apply(compiler);
@@ -136,14 +141,17 @@ function runForce(opts) {
   opts.compilation = {
     assets: {},
   };
-  // eslint-disable-next-line no-param-reassign
-  opts.compilation.assets[opts.existingAsset] = {
-    source() {
-      return 'existing';
-    },
-  };
 
-  return run(opts).then(() => {});
+  opts.existingAssets.forEach((assetName) => {
+    // eslint-disable-next-line no-param-reassign
+    opts.compilation.assets[assetName] = {
+      source() {
+        return 'existing';
+      },
+    };
+  });
+
+  return runEmit(opts).then(() => {});
 }
 
 function runChange(opts) {
@@ -163,7 +171,7 @@ function runChange(opts) {
 
   return run({
     compiler,
-    options: opts.options,
+    options: Object.assign({}, opts.options, { context: 'watch' }),
     patterns: opts.patterns,
   })
     .then(() => {