1const t = require('tap') 2const EventEmitter = require('events') 3const tmock = require('../../fixtures/tmock') 4const mockNpm = require('../../fixtures/mock-npm') 5 6const mockOpenUrlPrompt = async (t, { 7 questionShouldResolve = true, 8 openUrlPromptInterrupted = false, 9 openerResult = null, 10 isTTY = true, 11 emitter = null, 12 url: openUrl = 'https://www.npmjs.com', 13 ...config 14}) => { 15 const mock = await mockNpm(t, { 16 globals: { 17 'process.stdin.isTTY': isTTY, 18 'process.stdout.isTTY': isTTY, 19 }, 20 config, 21 }) 22 23 let openerUrl = null 24 let openerOpts = null 25 26 const openUrlPrompt = tmock(t, '{LIB}/utils/open-url-prompt.js', { 27 '@npmcli/promise-spawn': { 28 open: async (url, options) => { 29 openerUrl = url 30 openerOpts = options 31 if (openerResult) { 32 throw openerResult 33 } 34 }, 35 }, 36 readline: { 37 createInterface: () => ({ 38 question: (_q, cb) => { 39 if (questionShouldResolve === true) { 40 cb() 41 } 42 }, 43 close: () => {}, 44 on: (_signal, cb) => { 45 if (openUrlPromptInterrupted && _signal === 'SIGINT') { 46 cb() 47 } 48 }, 49 }), 50 }, 51 }) 52 53 let error 54 const args = [mock.npm, openUrl, 'npm home', 'prompt'] 55 if (emitter) { 56 mock.open = openUrlPrompt(...args, emitter) 57 } else { 58 await openUrlPrompt(...args).catch((er) => error = er) 59 } 60 61 return { 62 ...mock, 63 openerUrl, 64 openerOpts, 65 OUTPUT: mock.joinedOutput(), 66 emitter, 67 error, 68 } 69} 70 71t.test('does not open a url in non-interactive environments', async t => { 72 const { openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { isTTY: false }) 73 74 t.equal(openerUrl, null, 'did not open') 75 t.same(openerOpts, null, 'did not open') 76}) 77 78t.test('opens a url', async t => { 79 const { OUTPUT, openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { browser: true }) 80 81 t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url') 82 t.same(openerOpts, { command: null }, 'passed command as null (the default)') 83 t.matchSnapshot(OUTPUT) 84}) 85 86t.test('opens a url with browser string', async t => { 87 const { openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { browser: 'firefox' }) 88 89 t.equal(openerUrl, 'https://www.npmjs.com', 'opened the given url') 90 // FIXME: browser string is parsed as a boolean in config layer 91 // this is a bug that should be fixed or the config should not allow it 92 t.same(openerOpts, { command: null }, 'passed command as null (the default)') 93}) 94 95t.test('prints json output', async t => { 96 const { OUTPUT } = await mockOpenUrlPrompt(t, { json: true }) 97 98 t.matchSnapshot(OUTPUT) 99}) 100 101t.test('returns error for non-https url', async t => { 102 const { error, OUTPUT, openerUrl, openerOpts } = await mockOpenUrlPrompt(t, { 103 url: 'ftp://www.npmjs.com', 104 }) 105 106 t.match(error, /Invalid URL/, 'got the correct error') 107 t.equal(openerUrl, null, 'did not open') 108 t.same(openerOpts, null, 'did not open') 109 t.same(OUTPUT, '', 'printed no output') 110}) 111 112t.test('does not open url if canceled', async t => { 113 const emitter = new EventEmitter() 114 const { openerUrl, openerOpts, open } = await mockOpenUrlPrompt(t, { 115 questionShouldResolve: false, 116 emitter, 117 }) 118 119 emitter.emit('abort') 120 121 await open 122 123 t.equal(openerUrl, null, 'did not open') 124 t.same(openerOpts, null, 'did not open') 125}) 126 127t.test('returns error when opener errors', async t => { 128 const { error, openerUrl } = await mockOpenUrlPrompt(t, { 129 openerResult: new Error('Opener failed'), 130 }) 131 132 t.match(error, /Opener failed/, 'got the correct error') 133 t.equal(openerUrl, 'https://www.npmjs.com', 'did not open') 134}) 135 136t.test('throws "canceled" error on SIGINT', async t => { 137 const emitter = new EventEmitter() 138 const { open } = await mockOpenUrlPrompt(t, { 139 questionShouldResolve: false, 140 openUrlPromptInterrupted: true, 141 emitter, 142 }) 143 144 await t.rejects(open, /canceled/, 'message is canceled') 145}) 146