diff --git a/test/watch.js b/test/watch.js index 5b7b682..ed92113 100644 --- a/test/watch.js +++ b/test/watch.js @@ -10,216 +10,215 @@ import read from './helpers/read.js' import tmp from './helpers/tmp.js' // XXX: All the tests in this file are skipped on the Windows CI; too flacky there -const testCb = - process.env.CI && process.platform === 'win32' ? test.cb.skip : test.cb +const maybeTest = + process.env.CI && process.platform === 'win32' ? test.skip : test -testCb('--watch works', (t) => { - let cp +maybeTest('--watch works', async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() t.plan(2) - ENV('', ['a.css']) - .then((dir) => { - // Init watcher: - const watcher = chokidar.watch('.', { - cwd: dir, - ignoreInitial: true, - awaitWriteFinish: true, - }) + const dir = await ENV('', ['a.css']) - // On the first output: - watcher.on('add', (p) => { - // Assert, then change the source file - if (p === 'output.css') { - isEqual(p, 'test/fixtures/a.css') - .then(() => read('test/fixtures/b.css')) - .then((css) => fs.writeFile(path.join(dir, 'a.css'), css)) - .catch(done) - } - }) + // Init watcher: + const watcher = chokidar.watch('.', { + cwd: dir, + ignoreInitial: true, + awaitWriteFinish: true, + persistent: false, + }) - // When the change is picked up: - watcher.on('change', (p) => { - if (p === 'output.css') { - isEqual(p, 'test/fixtures/b.css') - .then(() => done()) - .catch(done) - } - }) + // On the first output: + watcher.on('add', (p) => { + // Assert, then change the source file + if (p === 'output.css') { + isEqual(p, 'test/fixtures/a.css') + .then(() => read('test/fixtures/b.css')) + .then((css) => fs.writeFile(path.join(dir, 'a.css'), css)) + .catch(reject) + } + }) - // Start postcss-cli: - watcher.on('ready', () => { - cp = spawn( - 'node', - [ - path.resolve('index.js'), - // '*.css' arrives as a single literal arg to test watch's glob handling - '*.css', - '-o', - 'output.css', - '--no-map', - '-w', - ], - { cwd: dir }, - ) - cp.on('error', t.end) - cp.on('exit', (code) => { - if (code) t.end(code) - }) - }) + // When the change is picked up: + watcher.on('change', (p) => { + if (p === 'output.css') { + isEqual(p, 'test/fixtures/b.css').then(resolve, reject) + } + }) - // Helper functions: - function isEqual(p, expected) { - return Promise.all([read(path.join(dir, p)), read(expected)]).then( - ([a, e]) => t.is(a, e), - ) - } + // Start postcss-cli: + watcher.on('ready', () => { + let processRunning = true + const cp = spawn( + 'node', + [ + path.resolve('index.js'), + // '*.css' arrives as a single literal arg to test watch's glob handling + '*.css', + '-o', + 'output.css', + '--no-map', + '-w', + ], + { cwd: dir }, + ) + const cleanup = () => { + try { + if (processRunning) cp.kill() + } catch {} + } + promise.then(cleanup, cleanup) + cp.on('error', (err) => { + processRunning = false + reject(err) + }) + cp.on('exit', (code) => { + processRunning = false + if (code) reject(`postcss-cli exited with code ${code}`) + }) + }) - function done(err) { - try { - cp.kill() - } catch {} + await promise - t.end(err) - } - }) - .catch(t.end) + // Helper functions: + function isEqual(p, expected) { + return Promise.all([read(path.join(dir, p)), read(expected)]).then( + ([a, e]) => t.is(a, e), + ) + } }) -testCb('--watch dependencies', (t) => { - let cp +maybeTest('--watch postcss-import dependencies', async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() t.plan(2) - ENV('', ['import.css', 'a.css']) - .then((dir) => { - // Init watcher: - const watcher = chokidar.watch('.', { - cwd: dir, - ignoreInitial: true, - awaitWriteFinish: true, - }) - - // On the first output: - watcher.on('add', (p) => { - // Assert, then change the source file - if (p === 'output.css') { - isEqual(p, 'test/fixtures/a.css') - .then(() => read('test/fixtures/b.css')) - .then((css) => fs.writeFile(path.join(dir, 'a.css'), css)) - .catch(done) - } - }) + const dir = await ENV('', ['import.css', 'a.css']) + // Init watcher: + const watcher = chokidar.watch('.', { + cwd: dir, + ignoreInitial: true, + awaitWriteFinish: true, + persistent: false, + }) - // When the change is picked up: - watcher.on('change', (p) => { - if (p === 'output.css') { - isEqual(p, 'test/fixtures/b.css') - .then(() => done()) - .catch(done) - } - }) + // On the first output: + watcher.on('add', (p) => { + // Assert, then change the source file + if (p === 'output.css') { + isEqual(p, 'test/fixtures/a.css') + .then(() => read('test/fixtures/b.css')) + .then((css) => fs.writeFile(path.join(dir, 'a.css'), css)) + .catch(reject) + } + }) - // Start postcss-cli: - watcher.on('ready', () => { - cp = spawn( - 'node', - [ - path.resolve('index.js'), - 'import.css', - '-o', - 'output.css', - '-u', - 'postcss-import', - '-w', - '--no-map', - ], - { cwd: dir }, - ) + // When the change is picked up: + watcher.on('change', (p) => { + if (p === 'output.css') { + isEqual(p, 'test/fixtures/b.css').then(resolve, reject) + } + }) - cp.on('error', t.end) - cp.on('exit', (code) => { - if (code) t.end(code) - }) - }) + // Start postcss-cli: + watcher.on('ready', () => { + let processRunning = true + const cp = spawn( + 'node', + [ + path.resolve('index.js'), + 'import.css', + '-o', + 'output.css', + '-u', + 'postcss-import', + '-w', + '--no-map', + ], + { cwd: dir }, + ) + const cleanup = () => { + try { + if (processRunning) cp.kill() + } catch {} + } + promise.then(cleanup, cleanup) + cp.on('error', (err) => { + processRunning = false + reject(err) + }) + cp.on('exit', (code) => { + processRunning = false + if (code) reject(`postcss-cli exited with code ${code}`) + }) + }) - // Helper functions: - function isEqual(p, expected) { - return Promise.all([read(path.join(dir, p)), read(expected)]).then( - ([a, e]) => t.is(a, e), - ) - } + await promise - function done(err) { - try { - cp.kill() - } catch {} - t.end(err) - } - }) - .catch(t.end) + // Helper functions: + function isEqual(p, expected) { + return Promise.all([read(path.join(dir, p)), read(expected)]).then( + ([a, e]) => t.is(a, e), + ) + } }) -testCb("--watch doesn't exit on CssSyntaxError", (t) => { - t.plan(0) +maybeTest("--watch doesn't exit on CssSyntaxError", async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() - ENV('', ['a.css']) - .then((dir) => { - // Init watcher: - const watcher = chokidar.watch('.', { - cwd: dir, - ignoreInitial: true, - awaitWriteFinish: true, - }) - watcher.on('add', (p) => { - if (p === 'output.css') { - // Change to invalid CSS - fs.writeFile(path.join(dir, 'a.css'), '.a { color: red').catch(done) - } - }) + t.plan(0) - let killed = false - const cp = spawn( - 'node', - [ - path.resolve('index.js'), - 'a.css', - '-o', - 'output.css', - '-u', - 'postcss-import', - '-w', - '--no-map', - ], - { cwd: dir }, - ) - cp.on('error', t.end) - cp.stderr.on('data', (chunk) => { - // When error message is printed, kill the process after a timeout - if (~chunk.indexOf('Unclosed block')) { - setTimeout(() => { - killed = true - cp.kill() - }, 1000) - } - }) - cp.on('exit', (code) => { - if (!killed) return t.end(`Should not exit (exited with code ${code})`) - done() - }) + const dir = await ENV('', ['a.css']) + // Init watcher: + const watcher = chokidar.watch('.', { + cwd: dir, + ignoreInitial: true, + awaitWriteFinish: true, + persistent: false, + }) + watcher.on('add', (p) => { + if (p === 'output.css') { + // Change to invalid CSS + fs.writeFile(path.join(dir, 'a.css'), '.a { color: red').catch(reject) + } + }) - function done(err) { - try { - cp.kill() - } catch {} + let killed = false + const cp = spawn( + 'node', + [ + path.resolve('index.js'), + 'a.css', + '-o', + 'output.css', + '-u', + 'postcss-import', + '-w', + '--no-map', + ], + { cwd: dir }, + ) + cp.on('error', reject) + cp.stderr.on('data', (chunk) => { + // When error message is printed, kill the process after a timeout + if (~chunk.indexOf('Unclosed block')) { + setTimeout(() => { + killed = true + cp.kill() + }, 1000) + } + }) + cp.on('exit', (code) => { + if (!killed) reject(`Should not exit (exited with code ${code})`) + else resolve() + }) - t.end(err) - } - }) - .catch(t.end) + await promise }) -testCb('--watch does exit on closing stdin (Ctrl-D/EOF)', (t) => { +maybeTest('--watch does exit on closing stdin (Ctrl-D/EOF)', async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() + t.plan(1) const cp = spawn( @@ -240,24 +239,26 @@ testCb('--watch does exit on closing stdin (Ctrl-D/EOF)', (t) => { }, ) - cp.on('error', t.end) + cp.on('error', reject) cp.on('exit', (code) => { t.is(code, 0) - t.end() + resolve() }) cp.stdin.end() + + await promise }) -testCb('--watch watches dependencies', (t) => { - let cp +maybeTest('--watch watches dependencies', async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() t.plan(2) - ENV('', ['s.css', 'a.css', 'b.css']).then((dir) => { - fs.writeFile( - path.join(dir, 'postcss.config.cjs'), - ` + const dir = await ENV('', ['s.css', 'a.css', 'b.css']) + await fs.writeFile( + path.join(dir, 'postcss.config.cjs'), + ` const fs = require('fs') module.exports = { plugins: [ @@ -276,294 +277,287 @@ testCb('--watch watches dependencies', (t) => { ] } `, - ) - .then(() => { - // Init watcher: - const watcher = chokidar.watch('.', { - cwd: dir, - ignoreInitial: true, - awaitWriteFinish: true, - }) - - // On the first output: - watcher.on('add', (p) => { - // Assert, then change the source file - if (p === 'output.css') { - isEqual(p, 'test/fixtures/a.css') - .then(() => read('test/fixtures/b.css')) - .then((css) => fs.writeFile(path.join(dir, 'a.css'), css)) - .catch(done) - } - }) - - // When the change is picked up: - watcher.on('change', (p) => { - if (p === 'output.css') { - isEqual(p, 'test/fixtures/b.css') - .then(() => done()) - .catch(done) - } - }) - - // Start postcss-cli: - watcher.on('ready', () => { - cp = spawn( - 'node', - [ - path.resolve('index.js'), - 's.css', - '-o', - 'output.css', - '--no-map', - '-w', - ], - { cwd: dir }, - ) - cp.on('error', t.end) - cp.on('exit', (code) => { - if (code) t.end(code) - }) - }) + ) - // Helper functions: - function isEqual(p, expected) { - return Promise.all([read(path.join(dir, p)), read(expected)]).then( - ([a, e]) => t.is(a, e), - ) - } + // Init watcher: + const watcher = chokidar.watch('.', { + cwd: dir, + ignoreInitial: true, + awaitWriteFinish: true, + persistent: false, + }) - function done(err) { - try { - cp.kill() - } catch {} + // On the first output: + watcher.on('add', (p) => { + // Assert, then change the source file + if (p === 'output.css') { + isEqual(p, 'test/fixtures/a.css') + .then(() => read('test/fixtures/b.css')) + .then((css) => fs.writeFile(path.join(dir, 'a.css'), css)) + .catch(reject) + } + }) - t.end(err) - } - }) - .catch(t.end) + // When the change is picked up: + watcher.on('change', (p) => { + if (p === 'output.css') { + isEqual(p, 'test/fixtures/b.css').then(resolve, reject) + } }) + + // Start postcss-cli: + watcher.on('ready', () => { + let processRunning = true + + const cp = spawn( + 'node', + [path.resolve('index.js'), 's.css', '-o', 'output.css', '--no-map', '-w'], + { cwd: dir }, + ) + const cleanup = () => { + try { + if (processRunning) cp.kill() + } catch {} + } + promise.then(cleanup, cleanup) + cp.on('error', (err) => { + processRunning = false + reject(err) + }) + cp.on('exit', (code) => { + processRunning = false + if (code) reject(`postcss-cli exited with code ${code}`) + }) + }) + + await promise + + // Helper functions: + function isEqual(p, expected) { + return Promise.all([read(path.join(dir, p)), read(expected)]).then( + ([a, e]) => t.is(a, e), + ) + } }) -testCb('--watch watches directory dependencies', (t) => { - let cp +maybeTest('--watch watches directory dependencies', async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() t.plan(2) - ENV('', ['s.css', 'base/level-1/b.css', 'base/level-1/level-2/a.css']).then( - (dir) => { - fs.writeFile( - path.join(dir, 'postcss.config.cjs'), - ` - const fs = require('fs') - module.exports = { - plugins: [ - (root, result) => { - result.messages.push({ - plugin: 'test', - type: 'dir-dependency', - dir: '${path.resolve(dir, 'base')}', - parent: result.opts.from, - }) - root.nodes = [] - root.append(fs.readFileSync('${path.resolve( - dir, - 'base/level-1/level-2/a.css', - )}', 'utf8')) - return root - } - ] - } - `, - ) - .then(() => { - // Init watcher: - const watcher = chokidar.watch('.', { - cwd: dir, - ignoreInitial: true, - awaitWriteFinish: true, + const dir = await ENV('', [ + 's.css', + 'base/level-1/b.css', + 'base/level-1/level-2/a.css', + ]) + await fs.writeFile( + path.join(dir, 'postcss.config.cjs'), + ` + const fs = require('fs') + module.exports = { + plugins: [ + (root, result) => { + result.messages.push({ + plugin: 'test', + type: 'dir-dependency', + dir: '${path.resolve(dir, 'base')}', + parent: result.opts.from, }) + root.nodes = [] + root.append(fs.readFileSync('${path.resolve( + dir, + 'base/level-1/level-2/a.css', + )}', 'utf8')) + return root + } + ] + } + `, + ) + // Init watcher: + const watcher = chokidar.watch('.', { + cwd: dir, + ignoreInitial: true, + awaitWriteFinish: true, + persistent: false, + }) - // On the first output: - watcher.on('add', (p) => { - // Assert, then change the source file - if (p === 'output.css') { - isEqual(p, 'test/fixtures/base/level-1/level-2/a.css') - .then(() => read('test/fixtures/base/level-1/b.css')) - .then((css) => - fs.writeFile( - path.join(dir, 'base/level-1/level-2/a.css'), - css, - ), - ) - .catch(done) - } - }) + // On the first output: + watcher.on('add', (p) => { + // Assert, then change the source file + if (p === 'output.css') { + isEqual(p, 'test/fixtures/base/level-1/level-2/a.css') + .then(() => read('test/fixtures/base/level-1/b.css')) + .then((css) => + fs.writeFile(path.join(dir, 'base/level-1/level-2/a.css'), css), + ) + .catch(reject) + } + }) - // When the change is picked up: - watcher.on('change', (p) => { - if (p === 'output.css') { - isEqual(p, 'test/fixtures/base/level-1/b.css') - .then(() => done()) - .catch(done) - } - }) + // When the change is picked up: + watcher.on('change', (p) => { + if (p === 'output.css') { + isEqual(p, 'test/fixtures/base/level-1/b.css').then(resolve, reject) + } + }) - // Start postcss-cli: - watcher.on('ready', () => { - cp = spawn( - 'node', - [ - path.resolve('index.js'), - 's.css', - '-o', - 'output.css', - '--no-map', - '-w', - ], - { cwd: dir }, - ) - cp.on('error', t.end) - cp.on('exit', (code) => { - if (code) t.end(code) - }) - }) + // Start postcss-cli: + watcher.on('ready', () => { + let processRunning = true - // Helper functions: - function isEqual(p, expected) { - return Promise.all([read(path.join(dir, p)), read(expected)]).then( - ([a, e]) => t.is(a, e), - ) - } - - function done(err) { - try { - cp.kill() - } catch {} - - t.end(err) - } - }) - .catch(t.end) - }, - ) + const cp = spawn( + 'node', + [path.resolve('index.js'), 's.css', '-o', 'output.css', '--no-map', '-w'], + { cwd: dir }, + ) + const cleanup = () => { + try { + if (processRunning) cp.kill() + } catch {} + } + promise.then(cleanup, cleanup) + cp.on('error', (err) => { + processRunning = false + reject(err) + }) + cp.on('exit', (code) => { + processRunning = false + if (code) reject(`postcss-cli exited with code ${code}`) + }) + }) + + await promise + + // Helper functions: + function isEqual(p, expected) { + return Promise.all([read(path.join(dir, p)), read(expected)]).then( + ([a, e]) => t.is(a, e), + ) + } }) -testCb( +maybeTest( '--watch applies glob on dir-dependency (and excludes non matching files)', - (t) => { - let cp + async (t) => { + const { promise, resolve, reject } = Promise.withResolvers() let modifying = null // one of "unrelated.md", "a.css" t.plan(1) - ENV('', [ + const dir = await ENV('', [ 's.css', 'base/level-1/b.css', 'base/level-1/level-2/a.css', 'base/level-1/level-2/unrelated.md', - ]).then((dir) => { - fs.writeFile( - path.join(dir, 'postcss.config.cjs'), - ` - const fs = require('fs') - module.exports = { - plugins: [ - (root, result) => { - result.messages.push({ - plugin: 'test', - type: 'dir-dependency', - dir: '${path.resolve(dir, 'base')}', - glob: '**/*.css', - parent: result.opts.from, - }) - root.nodes = [] - root.append(fs.readFileSync('${path.resolve( - dir, - 'base/level-1/level-2/a.css', - )}', 'utf8')) - return root - } - ] - } - `, - ) - .then(() => { - // Init watcher: - const watcher = chokidar.watch('.', { - cwd: dir, - ignoreInitial: true, - awaitWriteFinish: true, + ]) + await fs.writeFile( + path.join(dir, 'postcss.config.cjs'), + ` + const fs = require('fs') + module.exports = { + plugins: [ + (root, result) => { + result.messages.push({ + plugin: 'test', + type: 'dir-dependency', + dir: '${path.resolve(dir, 'base')}', + glob: '**/*.css', + parent: result.opts.from, }) + root.nodes = [] + root.append(fs.readFileSync('${path.resolve( + dir, + 'base/level-1/level-2/a.css', + )}', 'utf8')) + return root + } + ] + } + `, + ) - // On the first output: - watcher.on('add', (p) => { - if (p === 'output.css') { - // Modify unwatched file, shouldn't trigger output - modifyUnwatched() - } - }) + // Init watcher: + const watcher = chokidar.watch('.', { + cwd: dir, + ignoreInitial: true, + awaitWriteFinish: true, + persistent: false, + }) - // When the change is picked up: - watcher.on('change', (p) => { - if (p === 'output.css') { - // Assert that change to output.css happened only after modifying the watched a.css - t.is( - modifying, - 'a.css', - `Unexpected change to ${p} after modifying ${modifying}`, - ) - done() - } else if (p === 'base/level-1/level-2/unrelated.md') { - // Modify watched file next, should trigger output - setTimeout(modifyWatched, 250) - } - }) + // On the first output: + watcher.on('add', (p) => { + if (p === 'output.css') { + // Modify unwatched file, shouldn't trigger output + modifyUnwatched() + } + }) - // Start postcss-cli: - watcher.on('ready', () => { - cp = spawn( - 'node', - [ - path.resolve('index.js'), - 's.css', - '-o', - 'output.css', - '--no-map', - '-w', - ], - { cwd: dir }, - ) - cp.on('error', t.end) - cp.on('exit', (code) => { - if (code) t.end(code) - }) - }) + // When the change is picked up: + watcher.on('change', (p) => { + if (p === 'output.css') { + // Assert that change to output.css happened only after modifying the watched a.css + t.is( + modifying, + 'a.css', + `Unexpected change to ${p} after modifying ${modifying}`, + ) + resolve() + } else if (p === 'base/level-1/level-2/unrelated.md') { + // Modify watched file next, should trigger output + setTimeout(modifyWatched, 250) + } + }) + + // Start postcss-cli: + watcher.on('ready', () => { + let processRunning = true - function modifyUnwatched() { - modifying = 'unrelated.md' - fs.writeFile( - path.join(dir, 'base/level-1/level-2/unrelated.md'), - 'Some modification', - ).catch(done) - } - - function modifyWatched() { - modifying = 'a.css' - fs.writeFile( - path.join(dir, 'base/level-1/level-2/a.css'), - 'a { color: hotpink }', - ).catch(done) - } - - function done(err) { - try { - cp.kill() - } catch {} - - t.end(err) - } - }) - .catch(t.end) + const cp = spawn( + 'node', + [ + path.resolve('index.js'), + 's.css', + '-o', + 'output.css', + '--no-map', + '-w', + ], + { cwd: dir }, + ) + const cleanup = () => { + try { + if (processRunning) cp.kill() + } catch {} + } + promise.then(cleanup, cleanup) + cp.on('error', (err) => { + processRunning = false + reject(err) + }) + cp.on('exit', (code) => { + processRunning = false + if (code) reject(`postcss-cli exited with code ${code}`) + }) }) + + await promise + + // Helper functions: + function modifyUnwatched() { + modifying = 'unrelated.md' + fs.writeFile( + path.join(dir, 'base/level-1/level-2/unrelated.md'), + 'Some modification', + ).catch(reject) + } + + function modifyWatched() { + modifying = 'a.css' + fs.writeFile( + path.join(dir, 'base/level-1/level-2/a.css'), + 'a { color: hotpink }', + ).catch(reject) + } }, )