@@ -25,7 +25,7 @@ import Display from '../../../../src/commands/info/releasenotes/display';
2525// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2626chaiUse ( SinonChai ) ;
2727
28- describe ( 'info:releasenotes:display' , ( ) => {
28+ describe ( 'sfdx info:releasenotes:display' , ( ) => {
2929 const sandbox = Sinon . createSandbox ( ) ;
3030
3131 let mockInfoConfig : getInfoConfig . InfoConfig ;
@@ -264,3 +264,242 @@ describe('info:releasenotes:display', () => {
264264 expect ( json ) . to . deep . equal ( expected ) ;
265265 } ) ;
266266} ) ;
267+ describe ( 'sf info:releasenotes:display' , ( ) => {
268+ const sandbox = Sinon . createSandbox ( ) ;
269+
270+ let mockInfoConfig : getInfoConfig . InfoConfig ;
271+ let getBooleanStub : sinon . SinonStub ;
272+ let uxLogStub : sinon . SinonStub ;
273+ let uxWarnStub : sinon . SinonStub ;
274+ let getInfoConfigStub : Sinon . SinonStub ;
275+ let getReleaseNotesStub : Sinon . SinonStub ;
276+ let getDistTagVersionStub : Sinon . SinonStub ;
277+ let parseReleaseNotesSpy : Sinon . SinonSpy ;
278+ let markedParserSpy : Sinon . SinonSpy ;
279+
280+ const oclifConfigStub = fromStub ( stubInterface < Config > ( sandbox ) ) ;
281+
282+ class TestDisplay extends Display {
283+ public async runIt ( ) {
284+ await this . init ( ) ;
285+ return this . run ( ) ;
286+ }
287+ }
288+
289+ const runDisplayCmd = async ( params : string [ ] ) => {
290+ oclifConfigStub . bin = 'sf' ;
291+
292+ const cmd = new TestDisplay ( params , oclifConfigStub ) ;
293+
294+ uxLogStub = stubMethod ( sandbox , SfCommand . prototype , 'log' ) ;
295+ uxWarnStub = stubMethod ( sandbox , SfCommand . prototype , 'warn' ) ;
296+
297+ return cmd . runIt ( ) ;
298+ } ;
299+
300+ beforeEach ( ( ) => {
301+ mockInfoConfig = {
302+ releasenotes : {
303+ distTagUrl : 'https://registry.npmjs.org/-/package/sfdx-cli/dist-tags' ,
304+ releaseNotesPath : 'https://github.com/forcedotcom/cli/tree/main/releasenotes/sfdx' ,
305+ releaseNotesFilename : 'README.md' ,
306+ } ,
307+ } ;
308+
309+ oclifConfigStub . pjson . version = '3.3.3' ;
310+ oclifConfigStub . root = '/root/path' ;
311+
312+ getBooleanStub = stubMethod ( sandbox , Env . prototype , 'getBoolean' ) ;
313+ getBooleanStub . withArgs ( 'SF_HIDE_RELEASE_NOTES' ) . returns ( false ) ;
314+ getBooleanStub . withArgs ( 'SF_HIDE_RELEASE_NOTES_FOOTER' ) . returns ( false ) ;
315+
316+ getInfoConfigStub = stubMethod ( sandbox , getInfoConfig , 'getInfoConfig' ) . returns ( mockInfoConfig ) ;
317+ getReleaseNotesStub = stubMethod ( sandbox , getReleaseNotes , 'getReleaseNotes' ) . returns ( '## Release notes for 3.3.3' ) ;
318+ getDistTagVersionStub = stubMethod ( sandbox , getDistTagVersion , 'getDistTagVersion' ) . returns ( '1.2.3' ) ;
319+ parseReleaseNotesSpy = spyMethod ( sandbox , parseReleaseNotes , 'parseReleaseNotes' ) ;
320+ markedParserSpy = spyMethod ( sandbox , marked , 'parser' ) ;
321+ } ) ;
322+
323+ afterEach ( ( ) => {
324+ sandbox . restore ( ) ;
325+ } ) ;
326+
327+ it ( 'allows you to suppress release notes output with env var' , async ( ) => {
328+ // This env var is only honored when the --hook flag is passed.
329+ // If someone is running the command directly, we show notes regardless.
330+ getBooleanStub . withArgs ( 'SF_HIDE_RELEASE_NOTES' ) . returns ( true ) ;
331+
332+ const lifecycleStub = stubMethod ( sandbox , Lifecycle . prototype , 'emitTelemetry' ) ;
333+
334+ await runDisplayCmd ( [ '--hook' ] ) ;
335+
336+ expect ( lifecycleStub . calledOnce ) . to . equal ( true ) ;
337+ expect ( uxLogStub . called ) . to . be . false ;
338+ expect ( uxWarnStub . called ) . to . be . false ;
339+ } ) ;
340+
341+ it ( 'ignores hide release notes env var if running command directly (without --hook)' , async ( ) => {
342+ getBooleanStub . withArgs ( 'SF_HIDE_RELEASE_NOTES' ) . returns ( true ) ;
343+
344+ await runDisplayCmd ( [ ] ) ;
345+
346+ expect ( uxLogStub . args [ 0 ] [ 0 ] ) . to . contain ( '## Release notes for 3.3.3' ) ;
347+ } ) ;
348+
349+ it ( 'calls getInfoConfig with config root' , async ( ) => {
350+ await runDisplayCmd ( [ ] ) ;
351+
352+ expect ( getInfoConfigStub . args [ 0 ] [ 0 ] ) . to . equal ( '/root/path' ) ;
353+ } ) ;
354+
355+ it ( 'does not render emoji' , async ( ) => {
356+ getReleaseNotesStub . returns ( '## 3.3.3 :tada:' ) ;
357+
358+ await runDisplayCmd ( [ ] ) ;
359+
360+ expect ( uxLogStub . args [ 0 ] [ 0 ] ) . to . contain ( '## 3.3.3 :tada:' ) ;
361+ } ) ;
362+
363+ it ( 'throws an error if info config lookup fails' , async ( ) => {
364+ getInfoConfigStub . throws ( new Error ( 'info config error' ) ) ;
365+
366+ try {
367+ await shouldThrow ( runDisplayCmd ( [ ] ) ) ;
368+ } catch ( err ) {
369+ expect ( ( err as Error ) . message ) . to . contain ( 'info config error' ) ;
370+ }
371+ } ) ;
372+
373+ it ( 'does not call getDistTagVersion if helper is not passed' , async ( ) => {
374+ await runDisplayCmd ( [ ] ) ;
375+
376+ expect ( getDistTagVersionStub . called ) . to . be . false ;
377+ } ) ;
378+
379+ it ( 'calls getDistTagVersion with correct are if helpers are used' , async ( ) => {
380+ await runDisplayCmd ( [ '-v' , 'latest-rc' , '--hook' ] ) ;
381+
382+ expect ( getDistTagVersionStub . args [ 0 ] ) . to . deep . equal ( [ mockInfoConfig . releasenotes . distTagUrl , 'latest-rc' ] ) ;
383+ } ) ;
384+
385+ it ( 'throws an error if dist tag lookup fails' , async ( ) => {
386+ getDistTagVersionStub . throws ( new Error ( 'dist tag error' ) ) ;
387+
388+ try {
389+ await shouldThrow ( runDisplayCmd ( [ '-v' , 'latest-rc' ] ) ) ;
390+ } catch ( err ) {
391+ expect ( ( err as Error ) . message ) . to . contain ( 'dist tag error' ) ;
392+ }
393+ } ) ;
394+
395+ it ( 'calls getReleaseNotes with version returned from getDistTagVersion' , async ( ) => {
396+ await runDisplayCmd ( [ '-v' , 'latest-rc' , '--hook' ] ) ;
397+
398+ const expected = [
399+ mockInfoConfig . releasenotes . releaseNotesPath ,
400+ mockInfoConfig . releasenotes . releaseNotesFilename ,
401+ '1.2.3' ,
402+ ] ;
403+
404+ expect ( getReleaseNotesStub . args [ 0 ] ) . to . deep . equal ( expected ) ;
405+ } ) ;
406+
407+ it ( 'logs logs a header with cli bin' , async ( ) => {
408+ await runDisplayCmd ( [ ] ) ;
409+
410+ expect ( uxLogStub . args [ 0 ] [ 0 ] ) . to . contain ( "# Release notes for 'sf':" ) ;
411+ } ) ;
412+
413+ it ( 'calls getReleaseNotes with passed version' , async ( ) => {
414+ await runDisplayCmd ( [ '-v' , '4.5.6' , '--hook' ] ) ;
415+
416+ expect ( getReleaseNotesStub . args [ 0 ] [ 2 ] ) . to . equal ( '4.5.6' ) ;
417+ } ) ;
418+
419+ it ( 'calls getReleaseNotes with installed version if no arg is passed' , async ( ) => {
420+ await runDisplayCmd ( [ ] ) ;
421+
422+ expect ( getReleaseNotesStub . args [ 0 ] [ 2 ] ) . to . equal ( '3.3.3' ) ;
423+ } ) ;
424+
425+ it ( 'throws an error if getReleaseNotes lookup fails' , async ( ) => {
426+ getReleaseNotesStub . throws ( new Error ( 'release notes error' ) ) ;
427+
428+ try {
429+ await shouldThrow ( runDisplayCmd ( [ ] ) ) ;
430+ } catch ( err ) {
431+ expect ( ( err as Error ) . message ) . to . contain ( 'release notes error' ) ;
432+ }
433+ } ) ;
434+
435+ it ( 'parseReleaseNotes is called with the correct args' , async ( ) => {
436+ await runDisplayCmd ( [ ] ) ;
437+
438+ expect ( parseReleaseNotesSpy . args [ 0 ] ) . to . deep . equal ( [
439+ '## Release notes for 3.3.3' ,
440+ '3.3.3' ,
441+ mockInfoConfig . releasenotes . releaseNotesPath ,
442+ ] ) ;
443+ } ) ;
444+
445+ it ( 'parser is called with tokens' , async ( ) => {
446+ await runDisplayCmd ( [ ] ) ;
447+
448+ const tokens = parseReleaseNotesSpy . returnValues [ 0 ] as marked . Token ;
449+
450+ expect ( markedParserSpy . calledOnce ) . to . be . true ;
451+ expect ( markedParserSpy . args [ 0 ] [ 0 ] ) . to . deep . equal ( tokens ) ;
452+ } ) ;
453+
454+ it ( 'logs markdown on the command line' , async ( ) => {
455+ await runDisplayCmd ( [ ] ) ;
456+
457+ expect ( uxLogStub . args [ 0 ] [ 0 ] ) . to . contain ( '## Release notes for 3.3.3' ) ;
458+ } ) ;
459+
460+ it ( 'throws an error if parsing fails' , async ( ) => {
461+ try {
462+ await shouldThrow ( runDisplayCmd ( [ '-v' , '4.5.6' ] ) ) ;
463+ } catch ( err ) {
464+ expect ( ( err as Error ) . message ) . to . contain (
465+ `Didn't find version '4.5.6'. View release notes online at: ${ mockInfoConfig . releasenotes . releaseNotesPath } `
466+ ) ;
467+ }
468+ } ) ;
469+
470+ it ( 'does not throw an error if --hook is set' , async ( ) => {
471+ getReleaseNotesStub . throws ( new Error ( 'release notes error' ) ) ;
472+ const lifecycleStub = stubMethod ( sandbox , Lifecycle . prototype , 'emitTelemetry' ) ;
473+
474+ await runDisplayCmd ( [ '--hook' ] ) ;
475+ expect ( lifecycleStub . calledOnce ) . to . be . true ;
476+ expect ( uxWarnStub . args [ 0 ] [ 0 ] ) . to . contain ( 'release notes error' ) ;
477+ } ) ;
478+
479+ it ( 'renders a footer if --hook is set' , async ( ) => {
480+ await runDisplayCmd ( [ '--hook' ] ) ;
481+
482+ expect ( uxLogStub . args [ 1 ] [ 0 ] ) . to . contain ( 'to manually view the current release notes' ) ;
483+ } ) ;
484+
485+ it ( 'hides footer if env var is set' , async ( ) => {
486+ getBooleanStub . withArgs ( 'SF_HIDE_RELEASE_NOTES_FOOTER' ) . returns ( true ) ;
487+
488+ const lifecycleStub = stubMethod ( sandbox , Lifecycle . prototype , 'emitTelemetry' ) ;
489+
490+ await runDisplayCmd ( [ '--hook' ] ) ;
491+ expect ( lifecycleStub . calledOnce ) . to . be . true ;
492+ expect ( uxLogStub . args [ 2 ] ) . to . be . undefined ;
493+ } ) ;
494+
495+ it ( 'supports json output' , async ( ) => {
496+ const json = await runDisplayCmd ( [ '--json' ] ) ;
497+
498+ const expected = {
499+ body : `# Release notes for 'sf':${ os . EOL } ## Release notes for 3.3.3` ,
500+ url : mockInfoConfig . releasenotes . releaseNotesPath ,
501+ } ;
502+
503+ expect ( json ) . to . deep . equal ( expected ) ;
504+ } ) ;
505+ } ) ;
0 commit comments