rs-runner
Version:
RS is a CLI tool for quickly detecting package.json scripts, and running them.
413 lines (320 loc) • 11.9 kB
text/typescript
import { spawn, ChildProcess } from 'child_process';
import { output } from '../src/lib/output';
import * as pm from '../src/lib/pm';
import * as scripts from '../src/lib/scripts';
import {
runPackageScript,
runGlobalScript,
runDirectoryScript,
runRunnerCommand,
} from '../src/lib/run';
jest.mock('child_process');
jest.mock('../src/lib/output');
jest.mock('../src/lib/pm');
jest.mock('../src/lib/scripts');
describe('run module', () => {
let mockChildProcess: Partial<ChildProcess>;
beforeEach(() => {
jest.resetAllMocks();
mockChildProcess = {
on: jest.fn().mockReturnThis(),
};
(spawn as jest.Mock).mockReturnValue(mockChildProcess);
});
describe('runPackageScript', () => {
it('should execute script with detected runner', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
runPackageScript('test');
expect(output).toHaveBeenCalledWith('Executing: npm run test', 'green');
expect(spawn).toHaveBeenCalledWith('npm run test', {
stdio: 'inherit',
shell: true,
});
});
it('should use pnpm when detected', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('pnpm');
runPackageScript('build');
expect(spawn).toHaveBeenCalledWith('pnpm run build', {
stdio: 'inherit',
shell: true,
});
});
it('should use yarn when detected', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('yarn');
runPackageScript('dev');
expect(spawn).toHaveBeenCalledWith('yarn run dev', {
stdio: 'inherit',
shell: true,
});
});
it('should use bun when detected', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('bun');
runPackageScript('start');
expect(spawn).toHaveBeenCalledWith('bun run start', {
stdio: 'inherit',
shell: true,
});
});
it('should show error when no runner detected', () => {
(pm.detectRunner as jest.Mock).mockReturnValue(null);
runPackageScript('test');
expect(output.error).toHaveBeenCalledWith(
'No package manager detected. Run "npm init" or create a lock file first.',
);
expect(spawn).not.toHaveBeenCalled();
});
it('should handle spawn error', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
const error = new Error('spawn failed');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'error') {
callback(error);
}
return mockChildProcess;
});
runPackageScript('test');
expect(output.error).toHaveBeenCalledWith(
'Error executing script: spawn failed',
);
});
it('should handle non-zero exit code', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(1);
}
return mockChildProcess;
});
runPackageScript('test');
expect(output.error).toHaveBeenCalledWith('Script exited with code 1');
});
it('should not show error for zero exit code', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(0);
}
return mockChildProcess;
});
runPackageScript('test');
expect(output.error).not.toHaveBeenCalled();
});
it('should show error for null exit code (signal termination)', () => {
// Note: null exit code occurs when process is killed by signal.
// Current behavior shows error - could be improved to handle gracefully.
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(null);
}
return mockChildProcess;
});
runPackageScript('test');
expect(output.error).toHaveBeenCalledWith('Script exited with code null');
});
});
describe('runGlobalScript', () => {
it('should execute global script when found', () => {
(scripts.getGlobalScripts as jest.Mock).mockReturnValue({
lint: 'eslint .',
format: 'prettier --write .',
});
runGlobalScript('lint');
expect(output).toHaveBeenCalledWith(
'Executing global script: eslint .',
'green',
);
expect(spawn).toHaveBeenCalledWith('eslint .', {
stdio: 'inherit',
shell: true,
});
});
it('should warn when global script not found', () => {
(scripts.getGlobalScripts as jest.Mock).mockReturnValue({});
runGlobalScript('nonexistent');
expect(output.warn).toHaveBeenCalledWith(
"Global script 'nonexistent' not found. No global scripts defined.",
);
expect(spawn).not.toHaveBeenCalled();
});
it('should handle spawn error for global script', () => {
(scripts.getGlobalScripts as jest.Mock).mockReturnValue({
test: 'jest',
});
const error = new Error('command not found');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'error') {
callback(error);
}
return mockChildProcess;
});
runGlobalScript('test');
expect(output.error).toHaveBeenCalledWith(
'Error executing global script: command not found',
);
});
it('should handle non-zero exit for global script', () => {
(scripts.getGlobalScripts as jest.Mock).mockReturnValue({
lint: 'eslint .',
});
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(2);
}
return mockChildProcess;
});
runGlobalScript('lint');
expect(output.error).toHaveBeenCalledWith(
'Global script exited with code 2',
);
});
it('should not show error for zero exit code', () => {
(scripts.getGlobalScripts as jest.Mock).mockReturnValue({
lint: 'eslint .',
});
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(0);
}
return mockChildProcess;
});
runGlobalScript('lint');
expect(output.error).not.toHaveBeenCalled();
});
});
describe('runDirectoryScript', () => {
it('should execute directory script when found', () => {
(scripts.getDirectoryScripts as jest.Mock).mockReturnValue({
dev: 'vite dev',
build: 'vite build',
});
runDirectoryScript('dev');
expect(output).toHaveBeenCalledWith(
'Executing directory script: vite dev',
'green',
);
expect(spawn).toHaveBeenCalledWith('vite dev', {
stdio: 'inherit',
shell: true,
});
});
it('should warn when directory script not found', () => {
(scripts.getDirectoryScripts as jest.Mock).mockReturnValue({});
runDirectoryScript('nonexistent');
expect(output.warn).toHaveBeenCalledWith(
"Directory script 'nonexistent' not found for current directory.",
);
expect(spawn).not.toHaveBeenCalled();
});
it('should handle spawn error for directory script', () => {
(scripts.getDirectoryScripts as jest.Mock).mockReturnValue({
build: 'npm run build',
});
const error = new Error('failed to start');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'error') {
callback(error);
}
return mockChildProcess;
});
runDirectoryScript('build');
expect(output.error).toHaveBeenCalledWith(
'Error executing directory script: failed to start',
);
});
it('should handle non-zero exit for directory script', () => {
(scripts.getDirectoryScripts as jest.Mock).mockReturnValue({
build: 'npm run build',
});
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(127);
}
return mockChildProcess;
});
runDirectoryScript('build');
expect(output.error).toHaveBeenCalledWith(
'Directory script exited with code 127',
);
});
it('should not show error for zero exit code', () => {
(scripts.getDirectoryScripts as jest.Mock).mockReturnValue({
build: 'npm run build',
});
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(0);
}
return mockChildProcess;
});
runDirectoryScript('build');
expect(output.error).not.toHaveBeenCalled();
});
});
describe('runRunnerCommand', () => {
it('should execute runner command directly', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
runRunnerCommand('install lodash');
expect(output).toHaveBeenCalledWith(
'Executing command: npm install lodash',
'green',
);
expect(spawn).toHaveBeenCalledWith('npm install lodash', {
stdio: 'inherit',
shell: true,
});
});
it('should work with pnpm commands', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('pnpm');
runRunnerCommand('add -D typescript');
expect(spawn).toHaveBeenCalledWith('pnpm add -D typescript', {
stdio: 'inherit',
shell: true,
});
});
it('should show error when no runner detected', () => {
(pm.detectRunner as jest.Mock).mockReturnValue(null);
runRunnerCommand('install');
expect(output.error).toHaveBeenCalledWith(
'No package manager detected. Run "npm init" or create a lock file first.',
);
expect(spawn).not.toHaveBeenCalled();
});
it('should handle spawn error for runner command', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('yarn');
const error = new Error('yarn not installed');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'error') {
callback(error);
}
return mockChildProcess;
});
runRunnerCommand('install');
expect(output.error).toHaveBeenCalledWith(
'Error executing yarn command: yarn not installed',
);
});
it('should handle non-zero exit for runner command', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('bun');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(1);
}
return mockChildProcess;
});
runRunnerCommand('install');
expect(output.error).toHaveBeenCalledWith('bun command exited with code 1');
});
it('should not show error for zero exit code', () => {
(pm.detectRunner as jest.Mock).mockReturnValue('npm');
(mockChildProcess.on as jest.Mock).mockImplementation((event, callback) => {
if (event === 'exit') {
callback(0);
}
return mockChildProcess;
});
runRunnerCommand('install');
expect(output.error).not.toHaveBeenCalled();
});
});
});