diff options
author | Gar <gar+gh@danger.computer> | 2021-10-08 04:40:03 +0300 |
---|---|---|
committer | Gar <gar+gh@danger.computer> | 2021-11-04 00:04:22 +0300 |
commit | 8ffeb71dfb248b4a76744bd06cd4d6100f17c8ae (patch) | |
tree | b44d12a79dff3afe0c92df6f6c0219f6d91ad471 /test/lib/commands/view.js | |
parent | 85d59191cf681eabd8827ca58f925c1063776f61 (diff) |
chore: refactor commands
This is the first phase of refactoring the internal structure of the npm
commands to set us up for future changes. This iteration changes the
function signature of `exec` for all the commands to be a async (no more
callbacks), and also groups all the commands into their own
subdirectory.
It also removes the Proxy `npm.commands` object, in favor of an
`npm.cmd` and `npm.exec` function that breaks up the two things that
proxy was doing. Namely, getting to the attributes of a given command
(`npm.cmd` now does this), and actually running the command `npm.exec`
does this.
PR-URL: https://github.com/npm/cli/pull/3959
Credit: @wraithgar
Close: #3959
Reviewed-by: @lukekarrys
Diffstat (limited to 'test/lib/commands/view.js')
-rw-r--r-- | test/lib/commands/view.js | 660 |
1 files changed, 660 insertions, 0 deletions
diff --git a/test/lib/commands/view.js b/test/lib/commands/view.js new file mode 100644 index 000000000..116930aff --- /dev/null +++ b/test/lib/commands/view.js @@ -0,0 +1,660 @@ +const t = require('tap') + +t.cleanSnapshot = str => str.replace(/published .*? ago/g, 'published {TIME} ago') + +// run the same as tap does when running directly with node +process.stdout.columns = undefined + +const { fake: mockNpm } = require('../../fixtures/mock-npm') + +let logs +const cleanLogs = () => { + logs = '' + const fn = (...args) => { + logs += '\n' + args.map(el => logs += el) + } + console.log = fn +} + +// 25 hours ago +const yesterday = new Date(Date.now() - 1000 * 60 * 60 * 25) + +const packument = (nv, opts) => { + if (!opts.fullMetadata) + throw new Error('must fetch fullMetadata') + + if (!opts.preferOnline) + throw new Error('must fetch with preferOnline') + + const mocks = { + red: { + name: 'red', + 'dist-tags': { + '1.0.1': {}, + }, + time: { + unpublished: new Date(), + }, + }, + blue: { + name: 'blue', + 'dist-tags': { + latest: '1.0.0', + }, + time: { + '1.0.0': yesterday, + }, + versions: { + '1.0.0': { + name: 'blue', + version: '1.0.0', + dist: { + shasum: '123', + tarball: 'http://hm.blue.com/1.0.0.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + '1.0.1': { + name: 'blue', + version: '1.0.1', + dist: { + shasum: '124', + tarball: 'http://hm.blue.com/1.0.1.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + }, + }, + cyan: { + _npmUser: { + name: 'claudia', + email: 'claudia@cyan.com', + }, + name: 'cyan', + 'dist-tags': { + latest: '1.0.0', + }, + versions: { + '1.0.0': { + version: '1.0.0', + name: 'cyan', + dist: { + shasum: '123', + tarball: 'http://hm.cyan.com/1.0.0.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + '1.0.1': {}, + }, + }, + brown: { + name: 'brown', + }, + yellow: { + _id: 'yellow', + name: 'yellow', + author: { + name: 'foo', + email: 'foo@yellow.com', + twitter: 'foo', + }, + readme: 'a very useful readme', + versions: { + '1.0.0': { + version: '1.0.0', + author: 'claudia', + readme: 'a very useful readme', + maintainers: [ + { name: 'claudia', email: 'c@yellow.com', twitter: 'cyellow' }, + { name: 'isaacs', email: 'i@yellow.com', twitter: 'iyellow' }, + ], + }, + '1.0.1': { + version: '1.0.1', + author: 'claudia', + }, + '1.0.2': { + version: '1.0.2', + author: 'claudia', + }, + }, + }, + purple: { + name: 'purple', + versions: { + '1.0.0': { + foo: 1, + maintainers: [ + { name: 'claudia' }, + ], + }, + '1.0.1': {}, + }, + }, + green: { + _id: 'green', + name: 'green', + 'dist-tags': { + latest: '1.0.0', + }, + maintainers: [ + { name: 'claudia', email: 'c@yellow.com', twitter: 'cyellow' }, + { name: 'isaacs', email: 'i@yellow.com', twitter: 'iyellow' }, + ], + keywords: ['colors', 'green', 'crayola'], + versions: { + '1.0.0': { + _id: 'green', + version: '1.0.0', + description: 'green is a very important color', + bugs: { + url: 'http://bugs.green.com', + }, + deprecated: true, + repository: { + url: 'http://repository.green.com', + }, + license: { type: 'ACME' }, + bin: { + green: 'bin/green.js', + }, + dependencies: { + red: '1.0.0', + yellow: '1.0.0', + }, + dist: { + shasum: '123', + tarball: 'http://hm.green.com/1.0.0.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + '1.0.1': {}, + }, + }, + black: { + name: 'black', + 'dist-tags': { + latest: '1.0.0', + }, + versions: { + '1.0.0': { + version: '1.0.0', + bugs: 'http://bugs.black.com', + license: {}, + dependencies: (() => { + const deps = {} + for (let i = 0; i < 25; i++) + deps[i] = '1.0.0' + + return deps + })(), + dist: { + shasum: '123', + tarball: 'http://hm.black.com/1.0.0.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + '1.0.1': {}, + }, + }, + pink: { + name: 'pink', + 'dist-tags': { + latest: '1.0.0', + }, + versions: { + '1.0.0': { + version: '1.0.0', + maintainers: [ + { name: 'claudia', url: 'http://c.pink.com' }, + { name: 'isaacs', url: 'http://i.pink.com' }, + ], + repository: 'http://repository.pink.com', + license: {}, + dist: { + shasum: '123', + tarball: 'http://hm.pink.com/1.0.0.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + '1.0.1': {}, + }, + }, + orange: { + name: 'orange', + 'dist-tags': { + latest: '1.0.0', + }, + versions: { + '1.0.0': { + version: '1.0.0', + homepage: 'http://hm.orange.com', + license: {}, + dist: { + shasum: '123', + tarball: 'http://hm.orange.com/1.0.0.tgz', + integrity: '---', + fileCount: 1, + unpackedSize: 1, + }, + }, + '1.0.1': {}, + }, + }, + } + if (nv.type === 'git') + return mocks[nv.hosted.project] + return mocks[nv.name] +} + +t.beforeEach(cleanLogs) + +t.test('should log package info', async t => { + const View = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const npm = mockNpm({ + config: { unicode: false }, + }) + const view = new View(npm) + + const ViewJson = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const jsonNpm = mockNpm({ + config: { + json: true, + tag: 'latest', + }, + }) + const viewJson = new ViewJson(jsonNpm) + + const ViewUnicode = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const unicodeNpm = mockNpm({ + config: { unicode: true }, + }) + const viewUnicode = new ViewUnicode(unicodeNpm) + + t.test('package from git', async t => { + await view.exec(['https://github.com/npm/green']) + t.matchSnapshot(logs) + }) + + t.test('package with license, bugs, repository and other fields', async t => { + await view.exec(['green@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with more than 25 deps', async t => { + await view.exec(['black@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with maintainers info as object', async t => { + await view.exec(['pink@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with homepage', async t => { + await view.exec(['orange@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with no versions', async t => { + await view.exec(['brown']) + t.equal(logs, '', 'no info to display') + }) + + t.test('package with no repo or homepage', async t => { + await view.exec(['blue@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with semver range', async t => { + await view.exec(['blue@^1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with no modified time', async t => { + await viewUnicode.exec(['cyan@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with --json and semver range', async t => { + await viewJson.exec(['cyan@^1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('package with --json and no versions', async t => { + await viewJson.exec(['brown']) + t.equal(logs, '', 'no info to display') + }) +}) + +t.test('should log info of package in current working dir', async t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify({ + name: 'blue', + version: '1.0.0', + }, null, 2), + }) + + const View = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const npm = mockNpm({ + prefix: testDir, + config: { + tag: '1.0.0', + }, + }) + const view = new View(npm) + + t.test('specific version', async t => { + await view.exec(['.@1.0.0']) + t.matchSnapshot(logs) + }) + + t.test('non-specific version', async t => { + await view.exec(['.']) + t.matchSnapshot(logs) + }) +}) + +t.test('should log info by field name', async t => { + const ViewJson = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const jsonNpm = mockNpm({ + config: { + tag: 'latest', + json: true, + }, + }) + + const viewJson = new ViewJson(jsonNpm) + + const View = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const npm = mockNpm() + const view = new View(npm) + + t.test('readme', async t => { + await view.exec(['yellow@1.0.0', 'readme']) + t.matchSnapshot(logs) + }) + + t.test('several fields', async t => { + await viewJson.exec(['yellow@1.0.0', 'name', 'version', 'foo[bar]']) + t.matchSnapshot(logs) + }) + + t.test('several fields with several versions', async t => { + await view.exec(['yellow@1.x.x', 'author']) + t.matchSnapshot(logs) + }) + + t.test('nested field with brackets', async t => { + await viewJson.exec(['orange@1.0.0', 'dist[shasum]']) + t.matchSnapshot(logs) + }) + + t.test('maintainers with email', async t => { + await viewJson.exec(['yellow@1.0.0', 'maintainers', 'name']) + t.matchSnapshot(logs) + }) + + t.test('maintainers with url', async t => { + await viewJson.exec(['pink@1.0.0', 'maintainers']) + t.matchSnapshot(logs) + }) + + t.test('unknown nested field ', async t => { + await view.exec(['yellow@1.0.0', 'dist.foobar']) + t.equal(logs, '', 'no info to display') + }) + + t.test('array field - 1 element', async t => { + await view.exec(['purple@1.0.0', 'maintainers.name']) + t.matchSnapshot(logs) + }) + + t.test('array field - 2 elements', async t => { + await view.exec(['yellow@1.x.x', 'maintainers.name']) + t.matchSnapshot(logs) + }) +}) + +t.test('throw error if global mode', async t => { + const View = t.mock('../../../lib/commands/view.js') + const npm = mockNpm({ + config: { + global: true, + tag: 'latest', + }, + }) + const view = new View(npm) + await t.rejects( + view.exec([]), + /Cannot use view command in global mode./ + ) +}) + +t.test('throw ENOENT error if package.json missing', async t => { + const testDir = t.testdir({}) + + const View = t.mock('../../../lib/commands/view.js') + const npm = mockNpm({ + prefix: testDir, + }) + const view = new View(npm) + await t.rejects( + view.exec([]), + { code: 'ENOENT' } + ) +}) + +t.test('throw EJSONPARSE error if package.json not json', async t => { + const testDir = t.testdir({ + 'package.json': 'not json, nope, not even a little bit!', + }) + + const View = t.mock('../../../lib/commands/view.js') + const npm = mockNpm({ + prefix: testDir, + }) + const view = new View(npm) + await t.rejects( + view.exec([]), + { code: 'EJSONPARSE' } + ) +}) + +t.test('throw error if package.json has no name', async t => { + const testDir = t.testdir({ + 'package.json': '{}', + }) + + const View = t.mock('../../../lib/commands/view.js') + const npm = mockNpm({ + prefix: testDir, + }) + const view = new View(npm) + await t.rejects( + view.exec([]), + /Invalid package.json, no "name" field/ + ) +}) + +t.test('throws when unpublished', async t => { + const View = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const npm = mockNpm({ + config: { + tag: '1.0.1', + }, + }) + const view = new View(npm) + await t.rejects( + view.exec(['red']), + { code: 'E404'} + ) +}) + +t.test('workspaces', async t => { + t.beforeEach(() => { + warnMsg = undefined + config.json = false + }) + const testDir = t.testdir({ + 'package.json': JSON.stringify({ + name: 'workspaces-test-package', + version: '1.2.3', + workspaces: ['test-workspace-a', 'test-workspace-b'], + }), + 'test-workspace-a': { + 'package.json': JSON.stringify({ + name: 'green', + version: '1.2.3', + }), + }, + 'test-workspace-b': { + 'package.json': JSON.stringify({ + name: 'orange', + version: '1.2.3', + }), + }, + }) + const View = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const config = { + unicode: false, + tag: 'latest', + } + let warnMsg + const npm = mockNpm({ + log: { + warn: (msg) => { + warnMsg = msg + }, + }, + config, + localPrefix: testDir, + }) + const view = new View(npm) + + t.test('all workspaces', async t => { + await view.execWorkspaces([], []) + t.matchSnapshot(logs) + }) + + t.test('one specific workspace', async t => { + await view.execWorkspaces([], ['green']) + t.matchSnapshot(logs) + }) + + t.test('all workspaces --json', async t => { + config.json = true + await view.execWorkspaces([], []) + t.matchSnapshot(logs) + }) + + t.test('all workspaces single field', async t => { + await view.execWorkspaces(['.', 'name'], []) + t.matchSnapshot(logs) + }) + + t.test('all workspaces nonexistent field', async t => { + await view.execWorkspaces(['.', 'foo'], []) + t.matchSnapshot(logs) + }) + + t.test('all workspaces nonexistent field --json', async t => { + config.json = true + await view.execWorkspaces(['.', 'foo'], []) + t.matchSnapshot(logs) + }) + + t.test('all workspaces single field --json', async t => { + config.json = true + await view.execWorkspaces(['.', 'name'], []) + t.matchSnapshot(logs) + }) + + t.test('single workspace --json', async t => { + config.json = true + await view.execWorkspaces([], ['green']) + t.matchSnapshot(logs) + }) + + t.test('remote package name', async t => { + await view.execWorkspaces(['pink'], []) + t.matchSnapshot(warnMsg) + t.matchSnapshot(logs) + }) +}) + +t.test('completion', async t => { + const View = t.mock('../../../lib/commands/view.js', { + pacote: { + packument, + }, + }) + const npm = mockNpm({ + config: { + tag: '1.0.1', + }, + }) + const view = new View(npm) + const res = await view.completion({ + conf: { argv: { remain: ['npm', 'view', 'green@1.0.0'] } }, + }) + t.ok(res, 'returns back fields') +}) + +t.test('no registry completion', async t => { + const View = t.mock('../../../lib/commands/view.js') + const npm = mockNpm({ + config: { + tag: '1.0.1', + }, + }) + const view = new View(npm) + const res = await view.completion({conf: { argv: { remain: ['npm', 'view'] } } }) + t.notOk(res, 'there is no package completion') + t.end() +}) |