diff --git a/bin/jasmine.js b/bin/jasmine.js index 385ae02..c9f17d6 100755 --- a/bin/jasmine.js +++ b/bin/jasmine.js @@ -2,6 +2,7 @@ const path = require('path'); const os = require('os'); +const process = require('process'); const Command = require('../lib/command'); const Jasmine = require('../lib/jasmine'); const ParallelRunner = require("../lib/parallel_runner"); @@ -11,7 +12,12 @@ const command = new Command(path.resolve(), examplesDir, { Jasmine, ParallelRunner, print: console.log, + terminalColumns: process.stdout.columns, platform: os.platform, }); -command.run(process.argv.slice(2)); +command.run(process.argv.slice(2)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/lib/command.js b/lib/command.js index 151c580..461a1f2 100644 --- a/lib/command.js +++ b/lib/command.js @@ -27,7 +27,7 @@ const subCommands = { }; function Command(projectBaseDir, examplesDir, deps) { - const {print, platform, Jasmine, ParallelRunner} = deps; + const {print, platform, terminalColumns, Jasmine, ParallelRunner} = deps; const isWindows = platform() === 'win32'; this.projectBaseDir = isWindows ? unWindows(projectBaseDir) : projectBaseDir; @@ -54,7 +54,8 @@ function Command(projectBaseDir, examplesDir, deps) { projectBaseDir, specDir: command.specDir, examplesDir: examplesDir, - print + print, + terminalColumns }); } else { const options = parseOptions(args, isWindows); @@ -66,7 +67,7 @@ function Command(projectBaseDir, examplesDir, deps) { } print(''); - help({print: print}); + help({print, terminalColumns}); } else { await runJasmine(Jasmine, ParallelRunner, projectBaseDir, options); } @@ -284,9 +285,11 @@ function installExamples(options) { ); } -function help(options) { - const print = options.print; - print('Usage: jasmine [command] [options] [files] [--]'); +function help(deps) { + const print = deps.print; + let terminalColumns = deps.terminalColumns || 80; + + print(wrap(terminalColumns, 'Usage: jasmine [command] [options] [files] [--]')); print(''); print('Commands:'); Object.keys(subCommands).forEach(function(cmd) { @@ -294,29 +297,78 @@ function help(options) { if(subCommands[cmd].alias) { commandNameText = commandNameText + ',' + subCommands[cmd].alias; } - print('%s\t%s', lPad(commandNameText, 10), subCommands[cmd].description); + print(wrapWithIndent(terminalColumns, lPad(commandNameText, 10) + ' ', subCommands[cmd].description)); }); print(''); - print('If no command is given, jasmine specs will be run'); + print(wrap(terminalColumns, 'If no command is given, Jasmine specs will be run.')); print(''); print(''); print('Options:'); - print('%s\tRun in parallel with N workers', lPad('--parallel=N', 18)); - print('%s\tRun in parallel with an automatically chosen number of workers', lPad('--parallel=auto', 18)); - print('%s\tturn off color in spec output', lPad('--no-color', 18)); - print('%s\tforce turn on color in spec output', lPad('--color', 18)); - print('%s\tfilter specs to run only those that match the given string', lPad('--filter=', 18)); - print('%s\tload helper files that match the given string', lPad('--helper=', 18)); - print('%s\tload module that match the given string', lPad('--require=', 18)); - print('%s\tstop Jasmine execution on spec failure', lPad('--fail-fast', 18)); - print('%s\tpath to your optional jasmine.json', lPad('--config=', 18)); - print('%s\tpath to reporter to use instead of the default Jasmine reporter', lPad('--reporter=', 18)); - print('%s\tprint information that may be useful for debugging configuration', lPad('--verbose', 18)); - print('%s\tmarker to signal the end of options meant for Jasmine', lPad('--', 18)); + + const options = [ + { syntax: '--parallel=N', help: 'Run in parallel with N workers' }, + { syntax: '--parallel=auto', help: 'Run in parallel with an automatically chosen number of workers' }, + { syntax: '--no-color', help: 'turn off color in spec output' }, + { syntax: '--color', help: 'force turn on color in spec output' }, + { syntax: '--filter=', help: 'filter specs to run only those that match the given string' }, + { syntax: '--helper=', help: 'load helper files that match the given string' }, + { syntax: '--require=', help: 'load module that matches the given string' }, + { syntax: '--fail-fast', help: 'stop Jasmine execution on spec failure' }, + { syntax: '--config=', help: 'path to the Jasmine configuration file' }, + { syntax: '--reporter=', help: 'path to reporter to use instead of the default Jasmine reporter' }, + { syntax: '--verbose', help: 'print information that may be useful for debugging configuration' }, + { syntax: '--', help: 'marker to signal the end of options meant for Jasmine' }, + ]; + + for (const o of options) { + print(wrapWithIndent(terminalColumns, lPad(o.syntax, 18) + ' ', o.help)); + } + print(''); - print('The given arguments take precedence over options in your jasmine.json'); - print('The path to your optional jasmine.json can also be configured by setting the JASMINE_CONFIG_PATH environment variable'); + print(wrap(terminalColumns, + 'The given arguments take precedence over options in your jasmine.json.')); + print(wrap(terminalColumns, + 'The path to your optional jasmine.json can also be configured by setting the JASMINE_CONFIG_PATH environment variable.')); +} + +function wrapWithIndent(cols, prefix, suffix) { + const lines = wrap2(cols - prefix.length, suffix); + const indent = lPad('', prefix.length); + return prefix + lines.join('\n' + indent); +} + +function wrap(cols, input) { + return wrap2(cols, input).join('\n'); +} + +function wrap2(cols, input) { + let lines = []; + let start = 0; + + while (start < input.length) { + const splitAt = indexOfLastSpaceInRange(start, start + cols, input); + + if (splitAt === -1 || input.length - start <= cols) { + lines.push(input.substring(start)); + break; + } else { + lines.push(input.substring(start, splitAt)); + start = splitAt + 1; + } + } + + return lines; +} + +function indexOfLastSpaceInRange(start, end, s) { + for (let i = end; i >= start; i--) { + if (s[i] === ' ') { + return i; + } + } + + return -1; } function version(options) { diff --git a/spec/command_spec.js b/spec/command_spec.js index b46f17f..ced1250 100644 --- a/spec/command_spec.js +++ b/spec/command_spec.js @@ -44,7 +44,7 @@ describe('command', function() { let output = ""; return { print: function(str) { - output += str; + output += str + '\n'; }, getOutput: function() { return output; @@ -565,6 +565,52 @@ describe('command', function() { }); }); }); + + describe('help', function() { + it('wraps text to the terminal width', async function() { + this.command = new Command(projectBaseDir, '', { + print: this.out.print, + terminalColumns: 50, + platform() { + return 'arbitrary'; + } + }); + + await this.command.run(['help']); + + const output = this.out.getOutput(); + expect(output).toContain('version,-v show jasmine and jasmine-core\n' + + ' versions\n'); + expect(output).toContain(' --parallel=auto Run in parallel with an\n' + + ' automatically chosen number\n' + + ' of workers\n' + + ' --no-color turn off color in spec\n' + + ' output\n' + + ' --color force turn on color in spec\n' + + ' output\n'); + expect(output).toContain('The given arguments take precedence over options\n' + + 'in your jasmine.json.\n' + + 'The path to your optional jasmine.json can also be\n' + + 'configured by setting the JASMINE_CONFIG_PATH\n' + + 'environment variable.\n'); + }); + + it('wraps text to 80 columns when the terminal width is unknown', function() { + this.command = new Command(projectBaseDir, '', { + print: this.out.print, + terminalColumns: undefined, + platform() { + return 'arbitrary'; + } + }); + + this.command.run(['help']); + + const output = this.out.getOutput(); + expect(output).toContain(' --parallel=auto Run in parallel with an automatically chosen number of\n' + + ' workers\n'); + }); + }); }); // Adapted from Sindre Sorhus's escape-string-regexp (MIT license)