Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/pick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ export async function pickFiles(options: PickOptions): Promise<string> {
}

/**
* Get file content with markdown format (copied from main.ts but uses cwd)
* Get file content with markdown format
*/
async function getFileContent(
export async function getFileContent(
filePath: string,
maxLines?: number,
showLineNumbers?: boolean,
Expand Down
29 changes: 14 additions & 15 deletions tests/apply.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { writeFile, mkdir, stat, readFile } from 'fs/promises';
import { writeFile, mkdir, stat } from 'fs/promises';
import path from 'path';
import { parseCodeBlocks, applyFiles } from '../src/apply';

vi.mock('fs/promises');
Expand Down Expand Up @@ -73,9 +74,10 @@ And more ${bt(4)}more code${bt(4)}
});

it('identifies binary files', () => {
const input = `${bt(3)}png\n// image.png\n// [BINARY FILE] - Size: 1.000 MB\n${bt(3)}`;
const input = `${bt(3)}png\n// image.png\n\n\n// [BINARY FILE] - Size: 1.000 MB\n${bt(3)}`;

const files = parseCodeBlocks(input);
console.log(files);
expect(files).toHaveLength(1);
expect(files[0].isBinary).toBe(true);
});
Expand Down Expand Up @@ -210,7 +212,6 @@ describe('applyFiles', () => {
filePath: 'src/index.ts',
content: 'console.log("hello");',
isBinary: false,
isTruncated: false,
},
];

Expand All @@ -232,7 +233,6 @@ describe('applyFiles', () => {
filePath: 'src/index.ts',
content: 'console.log("updated");',
isBinary: false,
isTruncated: false,
},
];

Expand All @@ -248,14 +248,13 @@ describe('applyFiles', () => {
filePath: 'image.png',
content: '// [BINARY FILE]',
isBinary: true,
isTruncated: false,
},
];

const result = await applyFiles(files);

expect(result.skipped).toHaveLength(1);
expect(result.skipped[0]).toContain('image.png');
expect(result.skipped[0].path).toContain('image.png');
expect(writeFile).not.toHaveBeenCalled();
});

Expand All @@ -269,7 +268,6 @@ describe('applyFiles', () => {
filePath: 'large.ts',
content: 'line1\n// ... (100 more lines truncated)',
isBinary: false,
isTruncated: true,
},
];

Expand All @@ -288,15 +286,17 @@ describe('applyFiles', () => {
filePath: 'src/index.ts',
content: 'content',
isBinary: false,
isTruncated: false,
},
];

await applyFiles(files, '/custom/path');

expect(mkdir).toHaveBeenCalledWith('/custom/path/src', { recursive: true });
expect(mkdir).toHaveBeenCalledWith(
expect.stringContaining(path.join('custom', 'path', 'src')),
{ recursive: true },
);
expect(writeFile).toHaveBeenCalledWith(
'/custom/path/src/index.ts',
expect.stringContaining(path.join('custom', 'path', 'src', 'index.ts')),
'content',
'utf-8',
);
Expand All @@ -312,13 +312,15 @@ describe('applyFiles', () => {
filePath: 'src/deep/nested/file.ts',
content: 'content',
isBinary: false,
isTruncated: false,
},
];

await applyFiles(files);

expect(mkdir).toHaveBeenCalledWith('src/deep/nested', { recursive: true });
expect(mkdir).toHaveBeenCalledWith(
expect.stringContaining(path.join('src', 'deep', 'nested')),
{ recursive: true },
);
});

it('handles multiple files with mixed states', async () => {
Expand All @@ -334,19 +336,16 @@ describe('applyFiles', () => {
filePath: 'new.ts',
content: 'new',
isBinary: false,
isTruncated: false,
},
{
filePath: 'existing.ts',
content: 'updated',
isBinary: false,
isTruncated: false,
},
{
filePath: 'image.png',
content: '// [BINARY FILE]',
isBinary: true,
isTruncated: false,
},
];

Expand Down
74 changes: 38 additions & 36 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,43 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { readFile, stat } from 'fs/promises';
import glob from 'fast-glob';
import { isBinaryFile } from '../src/utils/binary';
import { addLineNumbers, formatSizeInMB } from '../src/utils/pipes';
import { findGitignoreFiles, loadGitignoreRules } from '../src/utils/gitignore';
import {
filterByGitignore,
findMaxConsecutiveBackticks,
getFileContent,
main,
} from '../src/main';
import { filterByIgnoreFile } from '../src/utils/ignore';
import { findMaxConsecutiveBackticks, getFileContent } from '../src/pick';
import { main } from '../src/main';

vi.mock('fs/promises');
vi.mock('fast-glob', () => ({ default: async () => [] }));
vi.mock('clipboardy');
vi.mock('fast-glob', () => ({ default: vi.fn() }));
vi.mock('clipboardy', () => ({ default: { read: vi.fn(), write: vi.fn() } }));
vi.mock('../src/utils/binary');
vi.mock('../src/utils/pipes');
vi.mock('../src/utils/gitignore');

describe('filterByGitignore', () => {
beforeEach(() => {
vi.resetAllMocks();
});

//
it('returns all files when no gitignore files found', async () => {
vi.mocked(findGitignoreFiles).mockResolvedValue([]);
vi.mocked(glob).mockResolvedValue([]);
const files = ['file1.ts', 'file2.ts'];
const result = await filterByGitignore(files);
const result = await filterByIgnoreFile(files, '.gitignore');
expect(result).toEqual(files);
});

//
it('filters files based on gitignore rules', async () => {
vi.mocked(findGitignoreFiles).mockResolvedValue(['.gitignore']);
vi.mocked(loadGitignoreRules).mockResolvedValue(['node_modules']);

const mockIg = { ignores: vi.fn().mockReturnValue(true) };
vi.doMock('ignore', () => () => mockIg);
vi.mocked(glob).mockResolvedValue(['.gitignore']);
vi.mocked(readFile).mockResolvedValue('node_modules');

const files = ['src/index.ts', 'node_modules/test.js'];
const result = await filterByGitignore(files);
expect(result).toHaveLength(1);
const result = await filterByIgnoreFile(files);
expect(result).toEqual(['src/index.ts']);
});

//
it('returns all files when gitignore processing fails', async () => {
vi.mocked(findGitignoreFiles).mockRejectedValue(new Error('Failed'));
vi.mocked(glob).mockRejectedValue(new Error('Failed'));
const files = ['file1.ts'];
const result = await filterByGitignore(files);
const result = await filterByIgnoreFile(files);
expect(result).toEqual(files);
});
});
Expand All @@ -73,8 +63,10 @@ describe('getFileContent', () => {

it('returns empty string on file read error', async () => {
vi.mocked(readFile).mockRejectedValue(new Error('Read error'));
const mockError = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await getFileContent('test.txt');
expect(result).toBe('');
mockError.mockRestore();
});

it('handles binary files', async () => {
Expand Down Expand Up @@ -133,29 +125,39 @@ describe('getFileContent', () => {
});

describe('CLI Integration', () => {
beforeEach(() => {
vi.resetAllMocks();
process.exitCode = 0;
});

it('exits with error when no patterns provided', async () => {
vi.mocked(findGitignoreFiles).mockResolvedValue([]);
const mockExit = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
const mockError = vi.spyOn(console, 'error').mockImplementation(() => {});

process.argv = ['node', 'codepicker'];
await main();

expect(mockExit).toHaveBeenCalledWith(1);
mockExit.mockRestore();
expect(process.exitCode).toBe(1);
expect(mockError).toHaveBeenCalledWith(
'✖ Error:',
'Provide at least one glob pattern.',
);

mockError.mockRestore();
});

it('exits with error when no files matched', async () => {
vi.mocked(findGitignoreFiles).mockResolvedValue([]);
const mockExit = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
vi.mocked(glob).mockResolvedValue([]);
const mockError = vi.spyOn(console, 'error').mockImplementation(() => {});

process.argv = ['node', 'codepicker', '**/*.ts'];
await main();

expect(mockExit).toHaveBeenCalledWith(1);
mockExit.mockRestore();
expect(process.exitCode).toBe(1);
expect(mockError).toHaveBeenCalledWith(
'✖ Error:',
'No files matched the given patterns.',
);

mockError.mockRestore();
});
});
27 changes: 12 additions & 15 deletions tests/utils/gitignore.test.ts → tests/utils/ignore.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,44 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { readFile } from 'fs/promises';
import {
findGitignoreFiles,
loadGitignoreRules,
} from '../../src/utils/gitignore';
import glob from 'fast-glob';
import { findIgnoreFiles, loadIgnoreRules } from '../../src/utils/ignore';

vi.mock('fs/promises');
vi.mock('fast-glob');
vi.mock('fast-glob', () => ({ default: vi.fn() }));

describe('findGitignoreFiles', () => {
describe('findIgnoreFiles', () => {
beforeEach(() => {
vi.resetAllMocks();
});

it('finds gitignore files', async () => {
it('finds ignore files', async () => {
const mockGlob = vi.mocked(glob);
mockGlob.mockResolvedValue(['.gitignore', 'src/.gitignore']);

const files = await findGitignoreFiles();
const files = await findIgnoreFiles('.gitignore');
expect(files).toEqual(['.gitignore', 'src/.gitignore']);
});

it('returns empty array on error', async () => {
const mockGlob = vi.mocked(glob);
mockGlob.mockRejectedValue(new Error('Failed'));

const files = await findGitignoreFiles();
const files = await findIgnoreFiles('.gitignore');
expect(files).toEqual([]);
});
});

describe('loadGitignoreRules', () => {
describe('loadIgnoreRules', () => {
beforeEach(() => {
vi.resetAllMocks();
});

it('loads rules from gitignore files', async () => {
it('loads rules from ignore files', async () => {
vi.mocked(readFile)
.mockResolvedValueOnce('node_modules\ndist\n!keep.js')
.mockResolvedValueOnce('*.log\ncache/');

const rules = await loadGitignoreRules(['.gitignore', 'src/.gitignore']);
const rules = await loadIgnoreRules(['.gitignore', 'src/.gitignore']);

expect(rules).toContain('node_modules');
expect(rules).toContain('dist');
Expand All @@ -55,7 +52,7 @@ describe('loadGitignoreRules', () => {
'# comment\n\nnode_modules\n# another\n*.js',
);

const rules = await loadGitignoreRules(['.gitignore']);
const rules = await loadIgnoreRules(['.gitignore']);

expect(rules).toEqual(['node_modules', '*.js']);
});
Expand All @@ -65,15 +62,15 @@ describe('loadGitignoreRules', () => {
.mockRejectedValueOnce(new Error('Failed'))
.mockResolvedValueOnce('*.log');

const rules = await loadGitignoreRules(['.gitignore', 'src/.gitignore']);
const rules = await loadIgnoreRules(['.gitignore', 'src/.gitignore']);

expect(rules).toEqual(['src/*.log']);
});

it('handles negated patterns correctly', async () => {
vi.mocked(readFile).mockResolvedValue('!important.js');

const rules = await loadGitignoreRules(['src/.gitignore']);
const rules = await loadIgnoreRules(['src/.gitignore']);

expect(rules).toEqual(['!src/important.js']);
});
Expand Down
Loading