mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
568 lines
19 KiB
TypeScript
568 lines
19 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import {
|
|
escapePath,
|
|
unescapePath,
|
|
isSubpath,
|
|
shortenPath,
|
|
normalizePath,
|
|
resolveToRealPath,
|
|
} from './paths.js';
|
|
|
|
vi.mock('node:fs', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof fs>();
|
|
return {
|
|
...(actual as object),
|
|
realpathSync: (p: string) => p,
|
|
};
|
|
});
|
|
|
|
const mockPlatform = (platform: string) => {
|
|
vi.stubGlobal(
|
|
'process',
|
|
Object.create(process, {
|
|
platform: {
|
|
get: () => platform,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
describe('escapePath', () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
describe('in posix', () => {
|
|
beforeEach(() => mockPlatform('linux'));
|
|
|
|
it.each([
|
|
['spaces', 'my file.txt', 'my\\ file.txt'],
|
|
['tabs', 'file\twith\ttabs.txt', 'file\\\twith\\\ttabs.txt'],
|
|
['parentheses', 'file(1).txt', 'file\\(1\\).txt'],
|
|
['square brackets', 'file[backup].txt', 'file\\[backup\\].txt'],
|
|
['curly braces', 'file{temp}.txt', 'file\\{temp\\}.txt'],
|
|
['semicolons', 'file;name.txt', 'file\\;name.txt'],
|
|
['ampersands', 'file&name.txt', 'file\\&name.txt'],
|
|
['pipes', 'file|name.txt', 'file\\|name.txt'],
|
|
['asterisks', 'file*.txt', 'file\\*.txt'],
|
|
['question marks', 'file?.txt', 'file\\?.txt'],
|
|
['dollar signs', 'file$name.txt', 'file\\$name.txt'],
|
|
['backticks', 'file`name.txt', 'file\\`name.txt'],
|
|
['single quotes', "file'name.txt", "file\\'name.txt"],
|
|
['double quotes', 'file"name.txt', 'file\\"name.txt'],
|
|
['hash symbols', 'file#name.txt', 'file\\#name.txt'],
|
|
['exclamation marks', 'file!name.txt', 'file\\!name.txt'],
|
|
['tildes', 'file~name.txt', 'file\\~name.txt'],
|
|
[
|
|
'less than and greater than signs',
|
|
'file<name>.txt',
|
|
'file\\<name\\>.txt',
|
|
],
|
|
[
|
|
'multiple special characters',
|
|
'my file (backup) [v1.2].txt',
|
|
'my\\ file\\ \\(backup\\)\\ \\[v1.2\\].txt',
|
|
],
|
|
['normal file', 'normalfile.txt', 'normalfile.txt'],
|
|
['normal path', 'path/to/normalfile.txt', 'path/to/normalfile.txt'],
|
|
[
|
|
'real world example 1',
|
|
'My Documents/Project (2024)/file [backup].txt',
|
|
'My\\ Documents/Project\\ \\(2024\\)/file\\ \\[backup\\].txt',
|
|
],
|
|
[
|
|
'real world example 2',
|
|
'file with $special &chars!.txt',
|
|
'file\\ with\\ \\$special\\ \\&chars\\!.txt',
|
|
],
|
|
['empty string', '', ''],
|
|
[
|
|
'all special chars',
|
|
' ()[]{};&|*?$`\'"#!<>',
|
|
'\\ \\(\\)\\[\\]\\{\\}\\;\\&\\|\\*\\?\\$\\`\\\'\\"\\#\\!\\<\\>',
|
|
],
|
|
])('should escape %s', (_, input, expected) => {
|
|
expect(escapePath(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('in windows', () => {
|
|
beforeEach(() => mockPlatform('win32'));
|
|
|
|
it.each([
|
|
[
|
|
'spaces',
|
|
'C:\\path with spaces\\file.txt',
|
|
'"C:\\path with spaces\\file.txt"',
|
|
],
|
|
['parentheses', 'file(1).txt', '"file(1).txt"'],
|
|
['special chars', 'file&name.txt', '"file&name.txt"'],
|
|
['caret', 'file^name.txt', '"file^name.txt"'],
|
|
['normal path', 'C:\\path\\to\\file.txt', 'C:\\path\\to\\file.txt'],
|
|
])('should escape %s', (_, input, expected) => {
|
|
expect(escapePath(input)).toBe(expected);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('unescapePath', () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
describe('in posix', () => {
|
|
beforeEach(() => mockPlatform('linux'));
|
|
|
|
it.each([
|
|
['spaces', 'my\\ file.txt', 'my file.txt'],
|
|
['tabs', 'file\\\twith\\\ttabs.txt', 'file\twith\ttabs.txt'],
|
|
['parentheses', 'file\\(1\\).txt', 'file(1).txt'],
|
|
['square brackets', 'file\\[backup\\].txt', 'file[backup].txt'],
|
|
['curly braces', 'file\\{temp\\}.txt', 'file{temp}.txt'],
|
|
[
|
|
'multiple special characters',
|
|
'my\\ file\\ \\(backup\\)\\ \\[v1.2\\].txt',
|
|
'my file (backup) [v1.2].txt',
|
|
],
|
|
['normal file', 'normalfile.txt', 'normalfile.txt'],
|
|
['normal path', 'path/to/normalfile.txt', 'path/to/normalfile.txt'],
|
|
['empty string', '', ''],
|
|
])('should unescape %s', (_, input, expected) => {
|
|
expect(unescapePath(input)).toBe(expected);
|
|
});
|
|
|
|
it.each([
|
|
'my file.txt',
|
|
'file(1).txt',
|
|
'file[backup].txt',
|
|
'My Documents/Project (2024)/file [backup].txt',
|
|
'file with $special &chars!.txt',
|
|
' ()[]{};&|*?$`\'"#!~<>',
|
|
'file\twith\ttabs.txt',
|
|
])('should unescape escaped %s', (input) => {
|
|
expect(unescapePath(escapePath(input))).toBe(input);
|
|
});
|
|
});
|
|
|
|
describe('in windows', () => {
|
|
beforeEach(() => mockPlatform('win32'));
|
|
|
|
it.each([
|
|
[
|
|
'quoted path',
|
|
'"C:\\path with spaces\\file.txt"',
|
|
'C:\\path with spaces\\file.txt',
|
|
],
|
|
['unquoted path', 'C:\\path\\to\\file.txt', 'C:\\path\\to\\file.txt'],
|
|
['partially quoted', '"C:\\path', '"C:\\path'],
|
|
['empty string', '', ''],
|
|
])('should unescape %s', (_, input, expected) => {
|
|
expect(unescapePath(input)).toBe(expected);
|
|
});
|
|
|
|
it.each([
|
|
'C:\\path\\to\\file.txt',
|
|
'C:\\path with spaces\\file.txt',
|
|
'file(1).txt',
|
|
'file&name.txt',
|
|
])('should unescape escaped %s', (input) => {
|
|
expect(unescapePath(escapePath(input))).toBe(input);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('isSubpath', () => {
|
|
it('should return true for a direct subpath', () => {
|
|
expect(isSubpath('/a/b', '/a/b/c')).toBe(true);
|
|
});
|
|
|
|
it('should return true for the same path', () => {
|
|
expect(isSubpath('/a/b', '/a/b')).toBe(true);
|
|
});
|
|
|
|
it('should return false for a parent path', () => {
|
|
expect(isSubpath('/a/b/c', '/a/b')).toBe(false);
|
|
});
|
|
|
|
it('should return false for a completely different path', () => {
|
|
expect(isSubpath('/a/b', '/x/y')).toBe(false);
|
|
});
|
|
|
|
it('should handle relative paths', () => {
|
|
expect(isSubpath('a/b', 'a/b/c')).toBe(true);
|
|
expect(isSubpath('a/b', 'a/c')).toBe(false);
|
|
});
|
|
|
|
it('should handle paths with ..', () => {
|
|
expect(isSubpath('/a/b', '/a/b/../b/c')).toBe(true);
|
|
expect(isSubpath('/a/b', '/a/c/../b')).toBe(true);
|
|
});
|
|
|
|
it('should handle root paths', () => {
|
|
expect(isSubpath('/', '/a')).toBe(true);
|
|
expect(isSubpath('/a', '/')).toBe(false);
|
|
});
|
|
|
|
it('should handle trailing slashes', () => {
|
|
expect(isSubpath('/a/b/', '/a/b/c')).toBe(true);
|
|
expect(isSubpath('/a/b', '/a/b/c/')).toBe(true);
|
|
expect(isSubpath('/a/b/', '/a/b/c/')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isSubpath on Windows', () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
beforeEach(() => mockPlatform('win32'));
|
|
|
|
it('should return true for a direct subpath on Windows', () => {
|
|
expect(isSubpath('C:\\Users\\Test', 'C:\\Users\\Test\\file.txt')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('should return true for the same path on Windows', () => {
|
|
expect(isSubpath('C:\\Users\\Test', 'C:\\Users\\Test')).toBe(true);
|
|
});
|
|
|
|
it('should return false for a parent path on Windows', () => {
|
|
expect(isSubpath('C:\\Users\\Test\\file.txt', 'C:\\Users\\Test')).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('should return false for a different drive on Windows', () => {
|
|
expect(isSubpath('C:\\Users\\Test', 'D:\\Users\\Test')).toBe(false);
|
|
});
|
|
|
|
it('should be case-insensitive for drive letters on Windows', () => {
|
|
expect(isSubpath('c:\\Users\\Test', 'C:\\Users\\Test\\file.txt')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('should be case-insensitive for path components on Windows', () => {
|
|
expect(isSubpath('C:\\Users\\Test', 'c:\\users\\test\\file.txt')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('should handle mixed slashes on Windows', () => {
|
|
expect(isSubpath('C:/Users/Test', 'C:\\Users\\Test\\file.txt')).toBe(true);
|
|
});
|
|
|
|
it('should handle trailing slashes on Windows', () => {
|
|
expect(isSubpath('C:\\Users\\Test\\', 'C:\\Users\\Test\\file.txt')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('should handle relative paths correctly on Windows', () => {
|
|
expect(isSubpath('Users\\Test', 'Users\\Test\\file.txt')).toBe(true);
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('resolveToRealPath', () => {
|
|
it.each([
|
|
{
|
|
description:
|
|
'should return path as-is if no special characters or protocol',
|
|
input: path.resolve('simple', 'path'),
|
|
expected: path.resolve('simple', 'path'),
|
|
},
|
|
{
|
|
description: 'should remove file:// protocol',
|
|
input: pathToFileURL(path.resolve('path', 'to', 'file')).toString(),
|
|
expected: path.resolve('path', 'to', 'file'),
|
|
},
|
|
{
|
|
description: 'should decode URI components',
|
|
input: path.resolve('path', 'to', 'some folder').replace(/ /g, '%20'),
|
|
expected: path.resolve('path', 'to', 'some folder'),
|
|
},
|
|
{
|
|
description: 'should handle both file protocol and encoding',
|
|
input: pathToFileURL(path.resolve('path', 'to', 'My Project')).toString(),
|
|
expected: path.resolve('path', 'to', 'My Project'),
|
|
},
|
|
])('$description', ({ input, expected }) => {
|
|
expect(resolveToRealPath(input)).toBe(expected);
|
|
});
|
|
|
|
it('should return decoded path even if fs.realpathSync fails', () => {
|
|
vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => {
|
|
throw new Error('File not found');
|
|
});
|
|
|
|
const p = path.resolve('path', 'to', 'New Project');
|
|
const input = pathToFileURL(p).toString();
|
|
const expected = p;
|
|
|
|
expect(resolveToRealPath(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('normalizePath', () => {
|
|
it('should resolve a relative path to an absolute path', () => {
|
|
const result = normalizePath('some/relative/path');
|
|
expect(result).toMatch(/^\/|^[a-z]:\//);
|
|
});
|
|
|
|
it('should convert all backslashes to forward slashes', () => {
|
|
const result = normalizePath(path.resolve('some', 'path'));
|
|
expect(result).not.toContain('\\');
|
|
});
|
|
|
|
describe.skipIf(process.platform !== 'win32')('on Windows', () => {
|
|
it('should lowercase the entire path', () => {
|
|
const result = normalizePath('C:\\Users\\TEST');
|
|
expect(result).toBe(result.toLowerCase());
|
|
});
|
|
|
|
it('should normalize drive letters to lowercase', () => {
|
|
const result = normalizePath('C:\\');
|
|
expect(result).toMatch(/^c:\//);
|
|
});
|
|
|
|
it('should handle mixed separators', () => {
|
|
const result = normalizePath('C:/Users\\Test/file.txt');
|
|
expect(result).not.toContain('\\');
|
|
expect(result).toMatch(/^c:\/users\/test\/file\.txt$/);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(process.platform === 'win32')('on POSIX', () => {
|
|
it('should preserve case', () => {
|
|
const result = normalizePath('/usr/Local/Bin');
|
|
expect(result).toContain('Local');
|
|
expect(result).toContain('Bin');
|
|
});
|
|
|
|
it('should use forward slashes', () => {
|
|
const result = normalizePath('/usr/local/bin');
|
|
expect(result).toBe('/usr/local/bin');
|
|
});
|
|
});
|
|
});
|