diff --git a/test/fixtures/watch/contents/change b/test/fixtures/watch/contents/change new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/watch/contents/dir/file b/test/fixtures/watch/contents/dir/file new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/watch/contents/remove b/test/fixtures/watch/contents/remove new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/watch/src/change b/test/fixtures/watch/src/change new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/watch/src/dir/file b/test/fixtures/watch/src/dir/file new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/watch/src/remove b/test/fixtures/watch/src/remove new file mode 100644 index 0000000..e69de29 diff --git a/test/watcher.js b/test/watcher.js new file mode 100644 index 0000000..1f46964 --- /dev/null +++ b/test/watcher.js @@ -0,0 +1,194 @@ +/* eslint-env node, mocha */ + +const { writeFile, rename } = require('fs/promises') +const assert = require('assert') +const path = require('path') +const { rm } = require('../lib/helpers') +const Metalsmith = require('..') +const fixture = path.resolve.bind(path, __dirname, 'fixtures') +const { platform } = require('os') + +const initialFiles = ['change', 'dir/file', 'remove'].map(path.normalize) + +const defaultWatchOpts = { + alwaysStat: false, + awaitWriteFinish: false, + cwd: fixture('watch'), + ignoreInitial: true, + ignored: [], + paths: fixture('watch/src') +} + +describe('watcher', function () { + let initialBuild = true + + this.timeout(5000) + this.beforeEach(() => { + initialBuild = true + }) + this.afterEach(async () => { + return Metalsmith(fixture('watch')).source('contents').destination('src').clean(true).build() + }) + + it('should set proper default options according to metalsmith config', function () { + const ms = Metalsmith(fixture('watch')) + ms.watch(true) + + assert.deepStrictEqual(ms.watch(), defaultWatchOpts) + }) + + it("should take into account metalsmith.ignore'd paths", function () { + const ms = Metalsmith(fixture('watch')) + ms.ignore('ignored') + ms.watch(true) + + assert.deepStrictEqual(ms.watch(), { ...defaultWatchOpts, ignored: ['ignored'] }) + }) + + it('should detect added files', async function () { + const addFile = async () => await writeFile(path.join(fixture('watch/src'), 'added'), '') + const ms = Metalsmith(fixture('watch')) + + return new Promise((resolve, reject) => { + ms.watch(true).build((err, files) => { + if (err) reject(err) + else { + if (initialBuild) { + initialBuild = false + addFile() + // this will only trigger if the if clause succeeds in triggering the chokidar watcher + } else { + ms.watch(false) + try { + assert.deepStrictEqual(Object.keys(files), [...initialFiles, 'added']) + resolve() + } catch (err) { + reject(err) + } + } + } + }) + }) + }) + + it('should detect removed files', async function () { + const remove = async () => await rm(path.join(fixture('watch/src'), 'remove')) + const ms = Metalsmith(fixture('watch')) + + return new Promise((resolve, reject) => { + ms.watch(true).build((err, files) => { + if (err) reject(err) + else { + if (initialBuild) { + initialBuild = false + remove() + // this will only trigger if the if clause succeeds in triggering the chokidar watcher + } else { + ms.watch(false) + try { + assert.deepStrictEqual(Object.keys(files), initialFiles.slice(0, 2)) + resolve() + } catch (err) { + reject(err) + } + } + } + }) + }) + }) + + it('should detect changed files', async function () { + const change = async () => await writeFile(path.join(fixture('watch/src'), 'change'), 'body') + const ms = Metalsmith(fixture('watch')) + + return new Promise((resolve, reject) => { + ms.watch(true).build((err, files) => { + if (err) reject(err) + else { + if (initialBuild) { + initialBuild = false + change() + // this will only trigger if the if clause succeeds in triggering the chokidar watcher + } else { + ms.watch(false) + try { + assert.strictEqual(files['change'].contents.toString(), 'body') + resolve() + } catch (err) { + reject(err) + } + } + } + }) + }) + }) + + it('should detect moved files', async function () { + const change = async () => + await rename(path.join(fixture('watch/src'), 'change'), path.join(fixture('watch/src'), 'renamed')) + const ms = Metalsmith(fixture('watch')) + initialBuild = 0 + + return new Promise((resolve, reject) => { + ms.watch(true).build((err, files) => { + if (err) reject(err) + else { + initialBuild += 1 + if (initialBuild === 1) { + change() + // this will only trigger if the if clause succeeds in triggering the chokidar watcher + } else { + if (initialBuild === 3) { + ms.watch(false) + try { + assert.deepStrictEqual(Object.keys(files), initialFiles.slice(1).concat(['renamed'])) + resolve() + } catch (err) { + reject(err) + } + } + } + } + }) + }) + }) + + it('should graciously batch file ops', async function () { + const batch = async () => { + rename(path.join(fixture('watch/src'), 'change'), path.join(fixture('watch/src'), 'renamed')) + writeFile(path.join(fixture('watch/src'), 'added'), 'body') + rm(path.join(fixture('watch/src'), 'remove')) + } + const ms = Metalsmith(fixture('watch')) + // windows file watchers are apparently slower, triggering an extra run + const lastBuild = platform() === 'win32' ? 4 : 3 + initialBuild = 0 + + return new Promise((resolve, reject) => { + ms.watch(true).build(async (err, files) => { + if (err) reject(err) + else { + initialBuild += 1 + if (initialBuild === 1) { + await batch() + // this will only trigger if the if clause succeeds in triggering the chokidar watcher + } else { + if (initialBuild === lastBuild) { + ms.watch(false) + try { + // doing the sort here because the order of execution of the batch is not guaranteed + assert.deepStrictEqual( + Object.keys(files).sort(), + initialFiles.slice(1, 2).concat(['added', 'renamed']).sort() + ) + resolve() + } catch (err) { + reject(err) + } + } + } + } + }) + }) + }) +})