From deb637ac1c15b06a6fefa581bc04ea2236be368b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 16:14:29 +0000 Subject: [PATCH] test: add unit tests for findFiles utility and improve extension tests - Extract findFiles() from PHPCBF class into lib/utils.js for testability - Add test/unit.test.js with 7 unit tests using Node.js built-in test runner covering: exact match, missing file, tree walk-up, deepest match wins, array of names, parent boundary, single-segment directory - Add npm script 'test:unit' to run unit tests without VS Code host - Improve test/extension.test.js: replace placeholder assertions with real tests for extension presence, activation, and command registration - No functional behaviour changes to extension.js Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extension.js | 24 +--------- lib/utils.js | 35 ++++++++++++++ package.json | 3 +- test/extension.test.js | 31 +++++++++--- test/unit.test.js | 106 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 lib/utils.js create mode 100644 test/unit.test.js diff --git a/extension.js b/extension.js index 81a61ce..6457e71 100644 --- a/extension.js +++ b/extension.js @@ -12,6 +12,7 @@ const path = require("path"); const fs = require("fs"); const os = require("os"); const cp = require("child_process"); +const { findFiles } = require("./lib/utils"); const TmpDir = os.tmpdir(); class PHPCBF { @@ -116,7 +117,7 @@ class PHPCBF { ]; const fileDir = path.relative(workspaceRoot, path.dirname(filePath)); - const confFile = this.findFiles(workspaceRoot, fileDir, confFileNames); + const confFile = findFiles(workspaceRoot, fileDir, confFileNames); standard = confFile || this.standard; } else { @@ -126,27 +127,6 @@ class PHPCBF { return standard; } - findFiles(parent, directory, name) { - const names = [].concat(name); - const chunks = path.resolve(parent, directory).split(path.sep); - - while (chunks.length) { - let currentDir = chunks.join(path.sep); - for (const fileName of names) { - const filePath = path.join(currentDir, fileName); - if (fs.existsSync(filePath)) { - return filePath; - } - } - if (parent === currentDir) { - break; - } - chunks.pop(); - } - - return null; - } - format(document) { // Reload settings scoped to this document so multi-root workspaces and // per-folder settings are respected on every format call. diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..5627bec --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,35 @@ +"use strict"; +const path = require("path"); +const fs = require("fs"); + +/** + * Walk up the directory tree from `parent/directory` looking for any file + * whose name matches one of the entries in `name`. Stops at `parent`. + * + * @param {string} parent - Root of the workspace (walk stops here). + * @param {string} directory - Relative directory to start from. + * @param {string|string[]} name - File name(s) to look for. + * @returns {string|null} Absolute path to the first matching file, or null. + */ +function findFiles(parent, directory, name) { + const names = [].concat(name); + const chunks = path.resolve(parent, directory).split(path.sep); + + while (chunks.length) { + let currentDir = chunks.join(path.sep); + for (const fileName of names) { + const filePath = path.join(currentDir, fileName); + if (fs.existsSync(filePath)) { + return filePath; + } + } + if (parent === currentDir) { + break; + } + chunks.pop(); + } + + return null; +} + +module.exports = { findFiles }; diff --git a/package.json b/package.json index c86425f..e64b8ed 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ }, "scripts": { "postinstall": "node ./node_modules/vscode/bin/install", - "test": "node ./node_modules/vscode/bin/test" + "test": "node ./node_modules/vscode/bin/test", + "test:unit": "node --test test/unit.test.js" }, "devDependencies": { "typescript": "^2.6.1", diff --git a/test/extension.test.js b/test/extension.test.js index c2ae013..c6bb04e 100644 --- a/test/extension.test.js +++ b/test/extension.test.js @@ -11,14 +11,33 @@ const assert = require('assert'); // You can import and use all API from the 'vscode' module // as well as import your extension to test it const vscode = require('vscode'); -const myExtension = require('../extension'); // Defines a Mocha test suite to group tests of similar kind together suite("Extension Tests", function() { - // Defines a Mocha unit test - test("Something 1", function() { - assert.equal(-1, [1, 2, 3].indexOf(5)); - assert.equal(-1, [1, 2, 3].indexOf(0)); + test("Extension is present", function() { + assert.ok(vscode.extensions.getExtension("persoderlind.phpcbf")); }); -}); \ No newline at end of file + + test("Extension activates for PHP documents", function(done) { + const ext = vscode.extensions.getExtension("persoderlind.phpcbf"); + if (!ext) { + // Extension not installed in test host — skip gracefully + done(); + return; + } + ext.activate().then( + () => { done(); }, + (err) => { done(err); } + ); + }); + + test("phpcbf-soderlind command is registered", function() { + return vscode.commands.getCommands(true).then(commands => { + assert.ok( + commands.includes("phpcbf-soderlind"), + "Expected phpcbf-soderlind command to be registered" + ); + }); + }); +}); diff --git a/test/unit.test.js b/test/unit.test.js new file mode 100644 index 0000000..0fb759e --- /dev/null +++ b/test/unit.test.js @@ -0,0 +1,106 @@ +"use strict"; +/** + * Unit tests for pure utility functions in lib/utils.js. + * Run with: npm run test:unit + * Requires Node.js 18+ (uses built-in test runner). + */ +const { test, describe, before, after } = require("node:test"); +const assert = require("node:assert/strict"); +const path = require("path"); +const fs = require("fs"); +const os = require("os"); + +const { findFiles } = require("../lib/utils"); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +let tmpRoot; + +function mkDir(...parts) { + const dir = path.join(tmpRoot, ...parts); + fs.mkdirSync(dir, { recursive: true }); + return dir; +} + +function mkFile(...parts) { + const filePath = path.join(tmpRoot, ...parts); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, ""); + return filePath; +} + +before(() => { + tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "phpcbf-unit-")); +}); + +after(() => { + fs.rmSync(tmpRoot, { recursive: true, force: true }); +}); + +// --------------------------------------------------------------------------- +// findFiles +// --------------------------------------------------------------------------- + +describe("findFiles", () => { + test("returns path when file exists in exact directory", () => { + const subDir = mkDir("exact", "sub"); + const expected = mkFile("exact", "sub", "phpcs.xml"); + const result = findFiles(path.join(tmpRoot, "exact"), "sub", "phpcs.xml"); + assert.equal(result, expected); + }); + + test("returns null when file does not exist anywhere in tree", () => { + mkDir("missing", "a", "b"); + const result = findFiles(path.join(tmpRoot, "missing"), path.join("a", "b"), "phpcs.xml"); + assert.equal(result, null); + }); + + test("walks up the directory tree to find file", () => { + // Place the config at the root (parent), start searching from deep subdir + const expected = mkFile("walkup", "phpcs.xml"); + mkDir("walkup", "a", "b", "c"); + const result = findFiles(path.join(tmpRoot, "walkup"), path.join("a", "b", "c"), "phpcs.xml"); + assert.equal(result, expected); + }); + + test("returns file in the deepest matching directory when multiple exist", () => { + // File exists at root AND a sub-directory — deepest should win because + // we search from deepest upward and return the first match. + mkFile("multi", "phpcs.xml"); + const deeper = mkFile("multi", "src", "phpcs.xml"); + const result = findFiles(path.join(tmpRoot, "multi"), "src", "phpcs.xml"); + assert.equal(result, deeper); + }); + + test("accepts an array of file names and returns first match found", () => { + mkDir("array", "sub"); + const expected = mkFile("array", "sub", ".phpcs.xml"); + const result = findFiles( + path.join(tmpRoot, "array"), + "sub", + [".phpcs.xml", "phpcs.xml", "phpcs.xml.dist"] + ); + assert.equal(result, expected); + }); + + test("stops at parent boundary — does not escape above parent", () => { + // The parent is "boundary/inner". A file exists one level above (in tmpRoot) + // but findFiles should NOT reach it. + mkFile("boundary-file.xml"); + mkDir("boundary", "inner"); + const result = findFiles( + path.join(tmpRoot, "boundary", "inner"), + ".", + "boundary-file.xml" + ); + assert.equal(result, null); + }); + + test("handles a single-segment directory (file at root)", () => { + const expected = mkFile("single", "phpcs.xml"); + const result = findFiles(path.join(tmpRoot, "single"), ".", "phpcs.xml"); + assert.equal(result, expected); + }); +});