Fix the shortenPath function to correctly insert ellipsis. (#12004)

Co-authored-by: Greg Shikhman <shikhman@google.com>
This commit is contained in:
ph-sp
2025-10-24 21:10:00 -07:00
committed by GitHub
parent 51578397a5
commit 73570f1c86
2 changed files with 372 additions and 27 deletions
+216 -1
View File
@@ -5,7 +5,7 @@
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { escapePath, unescapePath, isSubpath } from './paths.js';
import { escapePath, unescapePath, isSubpath, shortenPath } from './paths.js';
describe('escapePath', () => {
it.each([
@@ -257,3 +257,218 @@ describe('isSubpath on Windows', () => {
expect(isSubpath('Users\\Test\\file.txt', 'Users\\Test')).toBe(false);
});
});
describe('shortenPath', () => {
describe.skipIf(process.platform === 'win32')('on POSIX', () => {
it('should not shorten a path that is shorter than maxLen', () => {
const p = '/path/to/file.txt';
expect(shortenPath(p, 40)).toBe(p);
});
it('should not shorten a path that is equal to maxLen', () => {
const p = '/path/to/file.txt';
expect(shortenPath(p, p.length)).toBe(p);
});
it('should shorten a long path, keeping start and end from a short limit', () => {
const p = '/path/to/a/very/long/directory/name/file.txt';
expect(shortenPath(p, 25)).toBe('/path/.../name/file.txt');
});
it('should shorten a long path, keeping more from the end from a longer limit', () => {
const p = '/path/to/a/very/long/directory/name/file.txt';
expect(shortenPath(p, 35)).toBe('/path/.../directory/name/file.txt');
});
it('should handle deep paths where few segments from the end fit', () => {
const p = '/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/file.txt';
expect(shortenPath(p, 20)).toBe('/a/.../y/z/file.txt');
});
it('should handle deep paths where many segments from the end fit', () => {
const p = '/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/file.txt';
expect(shortenPath(p, 45)).toBe(
'/a/.../l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/file.txt',
);
});
it('should handle a long filename in the root when it needs shortening', () => {
const p = '/a-very-long-filename-that-needs-to-be-shortened.txt';
expect(shortenPath(p, 40)).toBe(
'/a-very-long-filen...o-be-shortened.txt',
);
});
it('should handle root path', () => {
const p = '/';
expect(shortenPath(p, 10)).toBe('/');
});
it('should handle a path with one long segment after root', () => {
const p = '/a-very-long-directory-name';
expect(shortenPath(p, 20)).toBe('/a-very-...ory-name');
});
it('should handle a path with just a long filename (no root)', () => {
const p = 'a-very-long-filename-that-needs-to-be-shortened.txt';
expect(shortenPath(p, 40)).toBe(
'a-very-long-filena...o-be-shortened.txt',
);
});
it('should fallback to truncating earlier segments while keeping the last intact', () => {
const p = '/abcdef/fghij.txt';
const result = shortenPath(p, 10);
expect(result).toBe('/fghij.txt');
expect(result.length).toBeLessThanOrEqual(10);
});
it('should fallback by truncating start and middle segments when needed', () => {
const p = '/averylongcomponentname/another/short.txt';
const result = shortenPath(p, 25);
expect(result).toBe('/averylo.../.../short.txt');
expect(result.length).toBeLessThanOrEqual(25);
});
it('should show only the last segment when maxLen is tiny', () => {
const p = '/foo/bar/baz.txt';
const result = shortenPath(p, 8);
expect(result).toBe('/baz.txt');
expect(result.length).toBeLessThanOrEqual(8);
});
it('should fall back to simple truncation when the last segment exceeds maxLen', () => {
const longFile = 'x'.repeat(60) + '.txt';
const p = `/really/long/${longFile}`;
const result = shortenPath(p, 50);
expect(result).toBe('/really/long/xxxxxxxxxx...xxxxxxxxxxxxxxxxxxx.txt');
expect(result.length).toBeLessThanOrEqual(50);
});
it('should handle relative paths without a root', () => {
const p = 'foo/bar/baz/qux.txt';
const result = shortenPath(p, 18);
expect(result).toBe('foo/.../qux.txt');
expect(result.length).toBeLessThanOrEqual(18);
});
it('should ignore empty segments created by repeated separators', () => {
const p = '/foo//bar///baz/verylongname.txt';
const result = shortenPath(p, 20);
expect(result).toBe('.../verylongname.txt');
expect(result.length).toBeLessThanOrEqual(20);
});
});
describe.skipIf(process.platform !== 'win32')('on Windows', () => {
it('should not shorten a path that is shorter than maxLen', () => {
const p = 'C\\Users\\Test\\file.txt';
expect(shortenPath(p, 40)).toBe(p);
});
it('should not shorten a path that is equal to maxLen', () => {
const p = 'C\\path\\to\\file.txt';
expect(shortenPath(p, p.length)).toBe(p);
});
it('should shorten a long path, keeping start and end from a short limit', () => {
const p = 'C\\path\\to\\a\\very\\long\\directory\\name\\file.txt';
expect(shortenPath(p, 30)).toBe('C\\...\\directory\\name\\file.txt');
});
it('should shorten a long path, keeping more from the end from a longer limit', () => {
const p = 'C\\path\\to\\a\\very\\long\\directory\\name\\file.txt';
expect(shortenPath(p, 42)).toBe(
'C\\...\\a\\very\\long\\directory\\name\\file.txt',
);
});
it('should handle deep paths where few segments from the end fit', () => {
const p =
'C\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q\\r\\s\\t\\u\\v\\w\\x\\y\\z\\file.txt';
expect(shortenPath(p, 22)).toBe('C\\...\\w\\x\\y\\z\\file.txt');
});
it('should handle deep paths where many segments from the end fit', () => {
const p =
'C\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q\\r\\s\\t\\u\\v\\w\\x\\y\\z\\file.txt';
expect(shortenPath(p, 47)).toBe(
'C\\...\\k\\l\\m\\n\\o\\p\\q\\r\\s\\t\\u\\v\\w\\x\\y\\z\\file.txt',
);
});
it('should handle a long filename in the root when it needs shortening', () => {
const p = 'C\\a-very-long-filename-that-needs-to-be-shortened.txt';
expect(shortenPath(p, 40)).toBe(
'C\\a-very-long-file...o-be-shortened.txt',
);
});
it('should handle root path', () => {
const p = 'C\\';
expect(shortenPath(p, 10)).toBe('C\\');
});
it('should handle a path with one long segment after root', () => {
const p = 'C\\a-very-long-directory-name';
expect(shortenPath(p, 22)).toBe('C\\a-very-...tory-name');
});
it('should handle a path with just a long filename (no root)', () => {
const p = 'a-very-long-filename-that-needs-to-be-shortened.txt';
expect(shortenPath(p, 40)).toBe(
'a-very-long-filena...o-be-shortened.txt',
);
});
it('should fallback to truncating earlier segments while keeping the last intact', () => {
const p = 'C\\abcdef\\fghij.txt';
const result = shortenPath(p, 15);
expect(result).toBe('C\\...\\fghij.txt');
expect(result.length).toBeLessThanOrEqual(15);
});
it('should fallback by truncating start and middle segments when needed', () => {
const p = 'C\\averylongcomponentname\\another\\short.txt';
const result = shortenPath(p, 30);
expect(result).toBe('C\\...\\another\\short.txt');
expect(result.length).toBeLessThanOrEqual(30);
});
it('should show only the last segment for tiny maxLen values', () => {
const p = 'C\\foo\\bar\\baz.txt';
const result = shortenPath(p, 12);
expect(result).toBe('...\\baz.txt');
expect(result.length).toBeLessThanOrEqual(12);
});
it('should keep the drive prefix when space allows', () => {
const p = 'C\\foo\\bar\\baz.txt';
const result = shortenPath(p, 14);
expect(result).toBe('C\\...\\baz.txt');
expect(result.length).toBeLessThanOrEqual(14);
});
it('should fall back when the last segment exceeds maxLen on Windows', () => {
const longFile = 'x'.repeat(60) + '.txt';
const p = `C\\really\\long\\${longFile}`;
const result = shortenPath(p, 40);
expect(result).toBe('C\\really\\long\\xxxx...xxxxxxxxxxxxxx.txt');
expect(result.length).toBeLessThanOrEqual(40);
});
it('should handle UNC paths with limited space', () => {
const p = '\\server\\share\\deep\\path\\file.txt';
const result = shortenPath(p, 25);
expect(result).toBe('\\server\\...\\path\\file.txt');
expect(result.length).toBeLessThanOrEqual(25);
});
it('should collapse UNC paths further when maxLen shrinks', () => {
const p = '\\server\\share\\deep\\path\\file.txt';
const result = shortenPath(p, 18);
expect(result).toBe('\\s...\\...\\file.txt');
expect(result.length).toBeLessThanOrEqual(18);
});
});
});