/* @flow */ import {existsSync} from 'fs'; import NoopReporter from '../src/reporters/base-reporter.js'; import makeTemp from './_temp'; import * as fs from '../src/util/fs.js'; const pkg = require('../package.json'); const path = require('path'); const exec = require('child_process').exec; const fixturesLoc = path.join(__dirname, './fixtures/index'); const yarnBin = path.join(__dirname, '../bin/yarn.js'); const semver = require('semver'); let ver = process.versions.node; ver = ver.split('-')[0]; jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; if (!existsSync(path.resolve(__dirname, '../lib'))) { throw new Error('These tests require `yarn build` to have been run first.'); } async function execCommand( cmd: string, args: Array, name: string, makeTempDir: ?boolean, ): Promise> { const srcDir = path.join(fixturesLoc, name); let workingDir = srcDir; if (makeTempDir) { workingDir = await makeTemp(name); await fs.copy(srcDir, workingDir, new NoopReporter()); } const cacheDir = path.join(workingDir, '.yarn-cache'); return new Promise((resolve, reject) => { const cleanedEnv = {...process.env}; cleanedEnv['YARN_SILENT'] = 0; cleanedEnv['YARN_WRAP_OUTPUT'] = 1; delete cleanedEnv['FORCE_COLOR']; exec( `node "${yarnBin}" --cache-folder="${cacheDir}" ${cmd} ${args.join(' ')}`, { cwd: workingDir, env: cleanedEnv, }, (error, stdout, stderr) => { if (error) { reject(Object.assign((new Error(error.message): any), {stdout, stderr})); } else { const stdoutLines = stdout .toString() .split('\n') .map((line: ?string) => line && line.trim()) .filter((line: ?string) => line); resolve(stdoutLines); } }, ); }); } function expectAddOutput(stdout) { expect(stdout.slice(1, stdout.length - 1)).toMatchSnapshot(); } function expectRunOutput(stdout) { const lastLines = stdout.slice(stdout.length - 2); expect(lastLines[0]).toMatch(/A message from custom script/); expect(lastLines[1]).toMatch(/^Done/); } function expectHelpOutput(stdout) { expect(stdout[0]).toEqual('Usage: yarn [command] [flags]'); const lastLines = stdout.slice(stdout.length - 2); expect(lastLines[0]).toEqual('Run `yarn help COMMAND` for more information on specific commands.'); expect(lastLines[1]).toEqual('Visit https://yarnpkg.com/en/docs/cli/ to learn more about Yarn.'); } function expectHelpOutputAsSubcommand(stdout) { expect(stdout[0]).toEqual('Usage: yarn add [packages ...] [flags]'); expect(stdout[stdout.length - 1]).toEqual( 'Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command.', ); } function expectAnErrorMessage(command: Promise>, expectedMessage: string): Promise { return command .then(function() { throw new Error('the command did not fail'); }) .catch(error => expect(error.message.replace(/\\/g, '')).toContain(expectedMessage)); } function expectAnInfoMessageAfterError(command: Promise>, expectedInfo: string): Promise { return command .then(function() { throw new Error('the command did not fail'); }) .catch(error => expect(error.stdout).toContain(expectedInfo)); } test('should add package', async () => { const stdout = await execCommand('add', ['left-pad@1.2.0'], 'run-add', true); expectAddOutput(stdout); }); test('should add package with no-lockfile option', async () => { const stdout = await execCommand('add', ['repeating@3.0.0', '--no-lockfile'], 'run-add-option', true); expectAddOutput(stdout); }); test('should add package with frozen-lockfile option', async () => { const stdout = await execCommand('add', ['repeating@3.0.0', '--frozen-lockfile'], 'run-add-option', true); expectAddOutput(stdout); }); test('should add package with no-lockfile option in front', async () => { const stdout = await execCommand('add', ['--no-lockfile', 'split-lines@1.1.0'], 'run-add-option-in-front', true); expectAddOutput(stdout); }); test('should add lockfile package', async () => { const stdout = await execCommand('add', ['lockfile@1.0.3'], 'run-add-lockfile', true); expectAddOutput(stdout); }); // test is failing on Node 4, https://travis-ci.org/yarnpkg/yarn/jobs/216254539 if (semver.satisfies(ver, '>=5.0.0')) { test.concurrent('should add progress package globally', async () => { const stdout = await execCommand( 'global', ['add', 'progress@2.0.0', '--global-folder', './global'], 'run-add-progress-globally', true, ); const lastLine = stdout[stdout.length - 1]; expect(lastLine).toMatch(/^Done/); }); } test.concurrent('should fail to find non-existent package offline', async () => { const command = execCommand( '--offline', ['global', 'add', 'doesnotexistqwertyuiop@2.0.0-doesnotexist', '--global-folder', './global'], 'run-global-add-offline', true, ); await expectAnErrorMessage( command, `error Couldn't find any versions for "doesnotexistqwertyuiop" that matches "2.0.0-doesnotexist" in our cache ` + '(possible versions are ""). This is usually caused by a missing entry in the lockfile, running Yarn without ' + 'the --offline flag may help fix this issue.', ); }); test.concurrent('should run custom script', async () => { const stdout = await execCommand('run', ['custom-script'], 'run-custom-script'); expectRunOutput(stdout); }); test.concurrent('should run custom script without run command', async () => { const stdout = await execCommand('custom-script', [], 'run-custom-script'); expectRunOutput(stdout); }); test.concurrent('should run without extra output for failing sub commands', async () => { try { await execCommand('run', ['--silent', 'custom-script'], 'run-failing-script'); throw new Error('the command did not fail'); } catch (err) { expect(err.stderr).toBe(''); } }); test.concurrent('should run help command', async () => { const stdout = await execCommand('help', [], 'run-help'); expectHelpOutput(stdout); }); test.concurrent('should run help command with --help', async () => { const stdout = await execCommand('--help', [], 'run-help'); expectHelpOutput(stdout); }); test.concurrent('should run help command with -h', async () => { const stdout = await execCommand('-h', [], 'run-help'); expectHelpOutput(stdout); }); test.concurrent('should run add command with help option', async () => { const stdout = await execCommand('add', ['--help'], 'run-help'); expectHelpOutputAsSubcommand(stdout); }); test.concurrent('should run add command with h option', async () => { const stdout = await execCommand('add', ['-h'], 'run-help'); expectHelpOutputAsSubcommand(stdout); }); test.concurrent('should run help command with add option', async () => { const stdout = await execCommand('help', ['add'], 'run-help'); expectHelpOutputAsSubcommand(stdout); }); test.concurrent('should run --help command with add option', async () => { const stdout = await execCommand('--help', ['add'], 'run-help'); expectHelpOutputAsSubcommand(stdout); }); test.concurrent('should run -h command with add option', async () => { const stdout = await execCommand('-h', ['add'], 'run-help'); expectHelpOutputAsSubcommand(stdout); }); test.concurrent('should show version of yarn with -v', async () => { const stdout = await execCommand('-v', [], 'run-version'); expect(stdout[0]).toEqual(pkg.version); }); test.concurrent('should run version command', async () => { const stdout = await execCommand('version', [], 'run-version'); expect(stdout[0]).toEqual(`yarn version v${pkg.version}`); }); test.concurrent('should run --version command', async () => { const stdout = await execCommand('--version', [], 'run-version'); expect(stdout[0]).toEqual(pkg.version); }); test.concurrent('should exit cleanly when running invalid commands', async () => { try { await execCommand('import', ['foobar'], 'run-version'); } catch (err) { // not important - we really only want to check that the command terminates } try { await execCommand('remove', ['foobar'], 'run-version'); } catch (err) { // not important - we really only want to check that the command terminates } }); test.concurrent('should install if no args', async () => { const stdout = await execCommand('', [], 'run-add', true); expect(stdout[0]).toEqual(`yarn install v${pkg.version}`); }); test.concurrent('should install if first arg looks like a flag', async () => { const stdout = await execCommand('--json', [], 'run-add', true); expect(stdout[stdout.length - 1]).toEqual('{"type":"success","data":"Saved lockfile."}'); }); test.concurrent('should not output JSON activity/progress if given --no-progress option', async () => { const activityInfo = ['activityStart', 'activityTick', 'activityEnd']; const progressInfo = ['progressStart', 'progressTick', 'progressFinish']; const stdout = await execCommand('', ['--json', '--no-progress'], 'run-add', true); stdout.forEach(line => { activityInfo.concat(progressInfo).forEach(info => { expect(line).not.toContain(info); }); }); }); test.concurrent('should run help of run command if --help is before script', async () => { const stdout = await execCommand('run', ['--help', 'custom-script'], 'run-custom-script-with-arguments'); expect(stdout[0]).toEqual('Usage: yarn [command] [flags]'); expect(stdout[stdout.length - 1]).toEqual( 'Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.', ); }); test.concurrent('should run help of custom-script if --help is after script', async () => { const stdout = await execCommand('run', ['--silent', 'custom-script', '--help'], 'run-custom-script-with-arguments'); expect(JSON.parse(stdout.join('\n'))).toContain('--help'); }); test.concurrent('should run bin command', async () => { const stdout = await execCommand('bin', [], '', true); expect(stdout[0]).toMatch(/[\\\/]node_modules[\\\/]\.bin\n?$/); expect(stdout.length).toEqual(1); }); test.concurrent('should throws missing command for not camelised command', async () => { await expectAnErrorMessage(execCommand('HelP', [], 'run-add', true), 'Command "HelP" not found'); }); test.concurrent('should throws missing command for not alphabetic command', async () => { await expectAnErrorMessage(execCommand('123', [], 'run-add', true), 'Command "123" not found'); }); test.concurrent('should throws missing command for unknown command', async () => { await expectAnErrorMessage(execCommand('unknown', [], 'run-add', true), 'Command "unknown" not found'); }); test.concurrent('should suggest options for other commands when missing a command', async () => { await expectAnErrorMessage(execCommand('foobarx', [], 'run-suggestion', true), 'Did you mean "foobar"?'); }); test.concurrent('should not display documentation link for unknown command', async () => { await expectAnInfoMessageAfterError(execCommand('unknown', [], 'run-add', true), ''); }); test.concurrent('should display documentation link for known command', async () => { await expectAnInfoMessageAfterError( execCommand('add', [], 'run-add', true), 'Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command.', ); }); test.concurrent('should throws missing command for constructor command', async () => { await expectAnErrorMessage(execCommand('constructor', [], 'run-add', true), 'Command "constructor" not found'); }); test.concurrent('should run command with hyphens', async () => { const stdout = await execCommand('generate-lock-entry', [], 'run-generate-lock-entry'); expect(stdout[0]).toMatch(/# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY./); expect(stdout[1]).toMatch(/# yarn lockfile v1/); }); test.concurrent('should run camelised command for command with hyphens', async () => { const stdout = await execCommand('generateLockEntry', [], 'run-generate-lock-entry'); expect(stdout[0]).toMatch(/# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY./); expect(stdout[1]).toMatch(/# yarn lockfile v1/); }); test.concurrent('should run help for command with hyphens', async () => { const stdout = await execCommand('generate-lock-entry', ['--help'], 'run-generate-lock-entry'); const lastLines = stdout.slice(stdout.length - 4); expect(lastLines[0]).toMatch(/yarn generate-lock-entry/); expect(lastLines[1]).toMatch(/yarn generate-lock-entry --use-manifest .\/package.json/); expect(lastLines[2]).toMatch(/yarn generate-lock-entry --resolved local-file.tgz#hash/); expect(lastLines[3]).toMatch(/Visit https:\/\/yarnpkg.com\/en\/docs\/cli\/generate-lock-entry/); }); test.concurrent('should run help for camelised command', async () => { const stdout = await execCommand('generateLockEntry', ['--help'], 'run-generate-lock-entry'); const lastLines = stdout.slice(stdout.length - 4); expect(lastLines[0]).toMatch(/yarn generate-lock-entry/); expect(lastLines[1]).toMatch(/yarn generate-lock-entry --use-manifest .\/package.json/); expect(lastLines[2]).toMatch(/yarn generate-lock-entry --resolved local-file.tgz#hash/); expect(lastLines[3]).toMatch(/Visit https:\/\/yarnpkg.com\/en\/docs\/cli\/generate-lock-entry/); }); // regression test for #5496 // this fixture has a .yarnrc in it that sets the `--emoji` flag. // we test to make sure that flag is not passed down to the workspace command, // but actual flags on the command line are passed. test.concurrent('should not pass yarnrc flags to workspace command', async () => { const stdout = await execCommand('workspace', ['workspace-1', 'run', 'check', '--x'], 'run-workspace', true); const params = stdout.find(x => x && x.indexOf('ARGS:') === 0); expect(params).not.toMatch(/emoji/); }); // regression test for #7776 test.concurrent('should pass args to workspace command without need for "--"', async () => { const stdout = await execCommand('workspace', ['workspace-1', 'run', 'check', '--x', 'y'], 'run-workspace', true); const params = stdout.find(x => x && x.indexOf('ARGS:') === 0); expect(params).toEqual('ARGS: --x y'); }); // regression test for #7776 test.concurrent('should pass args after "--" to workspace command', async () => { const stdout = await execCommand( 'workspace', ['workspace-1', 'run', 'check', '--', 'x', '-y'], 'run-workspace', true, ); const params = stdout.find(x => x && x.indexOf('ARGS:') === 0); expect(params).toEqual('ARGS: x -y'); }); // regression test for #7776 test.concurrent('should pass args to workspaces command without need for "--"', async () => { const stdout = await execCommand('workspaces', ['run', 'check', '--x', 'y'], 'run-workspace', true); const params = stdout.find(x => x && x.indexOf('ARGS:') === 0); expect(params).toEqual('ARGS: --x y'); }); // regression test for #7776 test.concurrent('should pass args after "--" to workspaces command', async () => { const stdout = await execCommand('workspaces', ['run', 'check', '--', 'x', '-y'], 'run-workspace', true); const params = stdout.find(x => x && x.indexOf('ARGS:') === 0); expect(params).toEqual('ARGS: x -y'); });