Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests(utils): add packageUtils tests #1375

Merged
merged 7 commits into from
Mar 25, 2020
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
2 changes: 1 addition & 1 deletion packages/package-utils/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

jest.mock('@webpack-cli/package-utils')
jest.mock('@webpack-cli/package-utils');

import { packageExists, promptInstallation } from '@webpack-cli/package-utils';
import ExternalCommand from '../../webpack-cli/lib/commands/ExternalCommand';
Expand Down
189 changes: 189 additions & 0 deletions packages/package-utils/__tests__/packageUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
'use strict';

jest.mock('execa');
jest.mock('cross-spawn');
const globalModulesNpmValue = 'test-npm';
jest.setMock('global-modules', globalModulesNpmValue);
jest.setMock('enquirer', {
prompt: jest.fn(),
});
jest.setMock('../lib/processUtils', {
runCommand: jest.fn(),
});

import fs from 'fs';
import path from 'path';
import execa from 'execa';
import spawn from 'cross-spawn';
import { prompt } from 'enquirer';
import { getPackageManager, packageExists } from '../lib/packageUtils';
import { runCommand } from '../lib/processUtils';

describe('packageUtils', () => {
describe('getPackageManager', () => {
const testYarnLockPath = path.resolve(__dirname, 'test-yarn-lock');
const testNpmLockPath = path.resolve(__dirname, 'test-npm-lock');
const testBothPath = path.resolve(__dirname, 'test-both');

const cwdSpy = jest.spyOn(process, 'cwd');

beforeAll(() => {
// mock sync
execa.sync = jest.fn();

// package-lock.json is ignored by .gitignore, so we simply
// write a lockfile here for testing
if (!fs.existsSync(testNpmLockPath)){
fs.mkdirSync(testNpmLockPath);
}
fs.writeFileSync(path.resolve(testNpmLockPath, 'package-lock.json'), '');
fs.writeFileSync(path.resolve(testBothPath, 'package-lock.json'), '');
});

beforeEach(() => {
(execa.sync as jest.Mock).mockClear();
});

it('should find yarn.lock', () => {
cwdSpy.mockReturnValue(testYarnLockPath);
expect(getPackageManager()).toEqual('yarn');
expect((execa.sync as jest.Mock).mock.calls.length).toEqual(0);
});

it('should find package-lock.json', () => {
cwdSpy.mockReturnValue(testNpmLockPath);
expect(getPackageManager()).toEqual('npm');
expect((execa.sync as jest.Mock).mock.calls.length).toEqual(0);
});

it('should prioritize yarn with many lock files', () => {
cwdSpy.mockReturnValue(testBothPath);
expect(getPackageManager()).toEqual('yarn');
expect((execa.sync as jest.Mock).mock.calls.length).toEqual(0);
});

it('should use yarn if yarn command works', () => {
// yarn should output a version number to stdout if
// it is installed
(execa.sync as jest.Mock).mockImplementation(() => {
return {
stdout: '1.0.0'
};
});
cwdSpy.mockReturnValue(path.resolve(__dirname));
expect(getPackageManager()).toEqual('yarn');
expect((execa.sync as jest.Mock).mock.calls.length).toEqual(1);
});

it('should use npm if yarn command fails', () => {
(execa.sync as jest.Mock).mockImplementation(() => {
throw new Error();
});
cwdSpy.mockReturnValue(path.resolve(__dirname));
expect(getPackageManager()).toEqual('npm');
expect((execa.sync as jest.Mock).mock.calls.length).toEqual(1);
});
});

describe('getPathToGlobalPackages', () => {
let packageUtils;
beforeAll(() => {
packageUtils = require('../lib/packageUtils');
packageUtils.getPackageManager = jest.fn();
});

it('uses global-modules if package manager is npm', () => {
packageUtils.getPackageManager.mockReturnValue('npm');
expect(packageUtils.getPathToGlobalPackages()).toEqual(globalModulesNpmValue);
});

it('executes a command to find yarn global dir if package manager is yarn', () => {
packageUtils.getPackageManager.mockReturnValue('yarn');
(spawn.sync as jest.Mock).mockReturnValue({
stdout: {
toString: (): string => {
return 'test-yarn';
},
},
});
// after the yarn global dir is found, the node_modules directory
// is added on to the path
expect(packageUtils.getPathToGlobalPackages()).toEqual('test-yarn/node_modules');
});
});

describe('packageExists', () => {
it('should check existence of package', () => {
// use an actual path relative to the packageUtils file
expect(packageExists('./processUtils')).toBeTruthy();
expect(packageExists('./nonexistent-package')).toBeFalsy();
});
});

describe('promptInstallation', () => {
let packageUtils;
beforeAll(() => {
packageUtils = require('../lib/packageUtils');
packageUtils.getPackageManager = jest.fn();
packageUtils.packageExists = jest.fn(() => true);
});

beforeEach(() => {
(runCommand as jest.Mock).mockClear();
(prompt as jest.Mock).mockClear();
});

it('should prompt to install using npm if npm is package manager', async () => {
packageUtils.getPackageManager.mockReturnValue('npm');
(prompt as jest.Mock).mockReturnValue({
installConfirm: true,
});

const preMessage = jest.fn();
const promptResult = await packageUtils.promptInstallation('test-package', preMessage);
expect(promptResult).toBeTruthy();
expect(preMessage.mock.calls.length).toEqual(1);
expect((prompt as jest.Mock).mock.calls.length).toEqual(1);
expect((runCommand as jest.Mock).mock.calls.length).toEqual(1);
expect((prompt as jest.Mock).mock.calls[0][0][0].message).toMatch(
/Would you like to install test\-package\?/
);
// install the package using npm
expect((runCommand as jest.Mock).mock.calls[0][0]).toEqual('npm install -D test-package');
});

it('should prompt to install using yarn if yarn is package manager', async () => {
packageUtils.getPackageManager.mockReturnValue('yarn');
(prompt as jest.Mock).mockReturnValue({
installConfirm: true,
});

const promptResult = await packageUtils.promptInstallation('test-package');
expect(promptResult).toBeTruthy();
expect((prompt as jest.Mock).mock.calls.length).toEqual(1);
expect((runCommand as jest.Mock).mock.calls.length).toEqual(1);
expect((prompt as jest.Mock).mock.calls[0][0][0].message).toMatch(
/Would you like to install test\-package\?/
);
// install the package using yarn
expect((runCommand as jest.Mock).mock.calls[0][0]).toEqual('yarn add -D test-package');
});

it('should not install if install is not confirmed', async () => {
packageUtils.getPackageManager.mockReturnValue('npm');
(prompt as jest.Mock).mockReturnValue({
installConfirm: false,
});

const promptResult = await packageUtils.promptInstallation('test-package');
expect(promptResult).toBeUndefined();
expect((prompt as jest.Mock).mock.calls.length).toEqual(1);
// runCommand should not be called, because the installation is not confirmed
expect((runCommand as jest.Mock).mock.calls.length).toEqual(0);
expect((prompt as jest.Mock).mock.calls[0][0][0].message).toMatch(
/Would you like to install test\-package\?/
);
expect(process.exitCode).toEqual(-1);
});
});
});
Empty file.
Empty file.
29 changes: 17 additions & 12 deletions packages/package-utils/src/packageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ type PackageName = 'npm' | 'yarn';

export function getPackageManager(): PackageName {
const hasLocalYarn = fs.existsSync(path.resolve(process.cwd(), 'yarn.lock'));
const hasLocalNpm = fs.existsSync(path.resolve(process.cwd(), 'package-lock.json'));

if (hasLocalYarn) {
return 'yarn';
} else if (hasLocalNpm) {
return 'npm';
}

try {
if (hasLocalYarn) {
return 'yarn';
} else if (sync('yarn', [' --version'], { stdio: 'ignore' }).stderr) {
// if the sync function below fails because yarn is not installed,
// an error will be thrown
if (sync('yarn', ['--version']).stdout) {
return 'yarn';
} else {
return 'npm';
}
} catch (e) {
return 'npm';
}
} catch (e) {}

return 'npm';
}

/**
Expand All @@ -40,8 +46,7 @@ export function getPackageManager(): PackageName {
* @returns {String} path - Path to global node_modules folder
*/
export function getPathToGlobalPackages(): string {
const manager: string = getPackageManager();

const manager: string = exports.getPackageManager();
if (manager === 'yarn') {
try {
const yarnDir = spawn
Expand Down Expand Up @@ -73,7 +78,7 @@ export function packageExists(packageName: string): boolean {
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function promptInstallation(packageName: string, preMessage?: Function) {
const packageManager = getPackageManager();
const packageManager = exports.getPackageManager();
const options = [packageManager === 'yarn' ? 'add' : 'install', '-D', packageName];

const commandToBeRun = `${packageManager} ${options.join(' ')}`;
Expand All @@ -91,7 +96,7 @@ export async function promptInstallation(packageName: string, preMessage?: Funct
]);
if (installConfirm) {
await runCommand(commandToBeRun);
return packageExists(packageName);
return exports.packageExists(packageName);
}
// eslint-disable-next-line require-atomic-updates
process.exitCode = -1;
Expand Down
6 changes: 3 additions & 3 deletions packages/webpack-cli/lib/commands/ExternalCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class ExternalCommand {
if (!pkgLoc) {
try {
pkgLoc = await promptInstallation(`${scopeName}`, () => {
logger.error(`The command moved into a separate package: ${chalk.keyword('orange')(scopeName)}\n`);
});
logger.error(`The command moved into a separate package: ${chalk.keyword('orange')(scopeName)}\n`);
});
} catch (err) {
logger.error(`Action Interrupted, use ${chalk.cyan(`webpack-cli help`)} to see possible commands.`)
logger.error(`Action Interrupted, use ${chalk.cyan(`webpack-cli help`)} to see possible commands.`)
}
}
return pkgLoc ? require(scopeName).default(...args) : null;
Expand Down