/** * Tests for scripts/lib/resolve-formatter.js * * Run with: node tests/lib/resolve-formatter.test.js */ const assert = require('assert'); const path = require('path'); const fs = require('fs'); const os = require('os'); const { findProjectRoot, detectFormatter, resolveFormatterBin, clearCaches } = require('../../scripts/lib/resolve-formatter'); /** * Run a single test case, printing pass/fail. * * @param {string} name - Test description * @param {() => void} fn - Test body (throws on failure) * @returns {boolean} Whether the test passed */ function test(name, fn) { try { fn(); console.log(` ✓ ${name}`); return true; } catch (err) { console.log(` ✗ ${name}`); console.log(` Error: ${err.message}`); return false; } } /** Track all created tmp dirs for cleanup */ const tmpDirs = []; /** * Create a temporary directory and track it for cleanup. * * @returns {string} Absolute path to the new temp directory */ function makeTmpDir() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'resolve-fmt-')); tmpDirs.push(dir); return dir; } /** * Remove all tracked temporary directories. */ function cleanupTmpDirs() { for (const dir of tmpDirs) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { // Best-effort cleanup } } tmpDirs.length = 0; } function runTests() { console.log('\n=== Testing resolve-formatter.js ===\n'); let passed = 0; let failed = 0; function run(name, fn) { clearCaches(); if (test(name, fn)) passed++; else failed++; } // ── findProjectRoot ─────────────────────────────────────────── run('findProjectRoot: finds package.json in parent dir', () => { const root = makeTmpDir(); const sub = path.join(root, 'src', 'lib'); fs.mkdirSync(sub, { recursive: true }); fs.writeFileSync(path.join(root, 'package.json'), '{}'); assert.strictEqual(findProjectRoot(sub), root); }); run('findProjectRoot: returns startDir when no package.json', () => { const root = makeTmpDir(); const sub = path.join(root, 'deep'); fs.mkdirSync(sub, { recursive: true }); // No package.json anywhere in tmp → falls back to startDir assert.strictEqual(findProjectRoot(sub), sub); }); run('findProjectRoot: caches result for same startDir', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'package.json'), '{}'); const first = findProjectRoot(root); // Remove package.json — cache should still return the old result fs.unlinkSync(path.join(root, 'package.json')); const second = findProjectRoot(root); assert.strictEqual(first, second); }); // ── detectFormatter ─────────────────────────────────────────── run('detectFormatter: detects biome.json', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'biome.json'), '{}'); assert.strictEqual(detectFormatter(root), 'biome'); }); run('detectFormatter: detects biome.jsonc', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'biome.jsonc'), '{}'); assert.strictEqual(detectFormatter(root), 'biome'); }); run('detectFormatter: detects .prettierrc', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, '.prettierrc'), '{}'); assert.strictEqual(detectFormatter(root), 'prettier'); }); run('detectFormatter: detects prettier.config.js', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'prettier.config.js'), 'module.exports = {}'); assert.strictEqual(detectFormatter(root), 'prettier'); }); run('detectFormatter: detects prettier key in package.json', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test', prettier: { singleQuote: true } })); assert.strictEqual(detectFormatter(root), 'prettier'); }); run('detectFormatter: ignores package.json without prettier key', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test' })); assert.strictEqual(detectFormatter(root), null); }); run('detectFormatter: biome takes priority over prettier', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'biome.json'), '{}'); fs.writeFileSync(path.join(root, '.prettierrc'), '{}'); assert.strictEqual(detectFormatter(root), 'biome'); }); run('detectFormatter: returns null when no config found', () => { const root = makeTmpDir(); assert.strictEqual(detectFormatter(root), null); }); // ── resolveFormatterBin ─────────────────────────────────────── run('resolveFormatterBin: uses local biome binary when available', () => { const root = makeTmpDir(); const binDir = path.join(root, 'node_modules', '.bin'); fs.mkdirSync(binDir, { recursive: true }); const binName = process.platform === 'win32' ? 'biome.cmd' : 'biome'; fs.writeFileSync(path.join(binDir, binName), ''); const result = resolveFormatterBin(root, 'biome'); assert.strictEqual(result.bin, path.join(binDir, binName)); assert.deepStrictEqual(result.prefix, []); }); run('resolveFormatterBin: falls back to npx for biome', () => { const root = makeTmpDir(); const result = resolveFormatterBin(root, 'biome'); const expectedBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; assert.strictEqual(result.bin, expectedBin); assert.deepStrictEqual(result.prefix, ['@biomejs/biome']); }); run('resolveFormatterBin: uses local prettier binary when available', () => { const root = makeTmpDir(); const binDir = path.join(root, 'node_modules', '.bin'); fs.mkdirSync(binDir, { recursive: true }); const binName = process.platform === 'win32' ? 'prettier.cmd' : 'prettier'; fs.writeFileSync(path.join(binDir, binName), ''); const result = resolveFormatterBin(root, 'prettier'); assert.strictEqual(result.bin, path.join(binDir, binName)); assert.deepStrictEqual(result.prefix, []); }); run('resolveFormatterBin: falls back to npx for prettier', () => { const root = makeTmpDir(); const result = resolveFormatterBin(root, 'prettier'); const expectedBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; assert.strictEqual(result.bin, expectedBin); assert.deepStrictEqual(result.prefix, ['prettier']); }); run('resolveFormatterBin: returns null for unknown formatter', () => { const root = makeTmpDir(); const result = resolveFormatterBin(root, 'unknown'); assert.strictEqual(result, null); }); run('resolveFormatterBin: caches resolved binary', () => { const root = makeTmpDir(); const binDir = path.join(root, 'node_modules', '.bin'); fs.mkdirSync(binDir, { recursive: true }); const binName = process.platform === 'win32' ? 'biome.cmd' : 'biome'; fs.writeFileSync(path.join(binDir, binName), ''); const first = resolveFormatterBin(root, 'biome'); fs.unlinkSync(path.join(binDir, binName)); const second = resolveFormatterBin(root, 'biome'); assert.strictEqual(first.bin, second.bin); }); // ── clearCaches ─────────────────────────────────────────────── run('clearCaches: clears all cached values', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'package.json'), '{}'); fs.writeFileSync(path.join(root, 'biome.json'), '{}'); findProjectRoot(root); detectFormatter(root); resolveFormatterBin(root, 'biome'); clearCaches(); // After clearing, removing config should change detection fs.unlinkSync(path.join(root, 'biome.json')); assert.strictEqual(detectFormatter(root), null); }); // ── Summary & Cleanup ───────────────────────────────────────── cleanupTmpDirs(); console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); console.log(`Failed: ${failed}`); console.log(`Total: ${passed + failed}`); process.exit(failed > 0 ? 1 : 0); } runTests();