From 4459a33eac020f57e929f87b94b8fc8c07d34c55 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 16 Feb 2023 09:23:05 -0700 Subject: [PATCH 1/8] feat: export Spinner and remove console.error --- src/exported.ts | 2 +- src/sfCommand.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/exported.ts b/src/exported.ts index bf9ad434a..26aedd55a 100644 --- a/src/exported.ts +++ b/src/exported.ts @@ -10,7 +10,7 @@ import { Flags as OclifFlags } from '@oclif/core'; export { toHelpSection, parseVarArgs } from './util'; export { Deployable, Deployer, DeployerResult } from './deployer'; export { Deauthorizer } from './deauthorizer'; -export { Progress, Prompter, generateTableChoices, Ux } from './ux'; +export { Progress, Prompter, generateTableChoices, Ux, Spinner } from './ux'; export { SfHook } from './hooks'; export * from './types'; export { SfCommand, SfCommandInterface, StandardColors } from './sfCommand'; diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 2136a65fb..87bcf1511 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -351,7 +351,7 @@ export abstract class SfCommand extends Command { return this.prompter.timedPrompt(questions, ms, initialAnswers); } - public async _run(): Promise { + public async _run(): Promise { this.configAggregator = this.config.bin === 'sfdx' ?? env.getBoolean('SF_USE_DEPRECATED_CONFIG_VARS') ?? @@ -444,8 +444,7 @@ export abstract class SfCommand extends Command { if (this.jsonEnabled()) { ux.styledJSON(this.toErrorJson(sfCommandError)); } else { - // eslint-disable-next-line no-console - console.error(this.formatError(sfCommandError)); + this.logToStderr(this.formatError(sfCommandError)); } return sfCommandError; } From f72a577692644a09dc7007e9530a6faf7e12d26c Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 17 Feb 2023 12:54:46 -0700 Subject: [PATCH 2/8] feat: add stubUx --- src/exported.ts | 1 + src/stubUx.ts | 45 ++++++ test/unit/stubUx.test.ts | 336 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 src/stubUx.ts create mode 100644 test/unit/stubUx.test.ts diff --git a/src/exported.ts b/src/exported.ts index 26aedd55a..c0def1c7f 100644 --- a/src/exported.ts +++ b/src/exported.ts @@ -15,6 +15,7 @@ export { SfHook } from './hooks'; export * from './types'; export { SfCommand, SfCommandInterface, StandardColors } from './sfCommand'; export * from './compatibility'; +export * from './stubUx'; // custom flags import { requiredOrgFlag, requiredHubFlag, optionalOrgFlag, optionalHubFlag } from './flags/orgFlags'; import { salesforceIdFlag } from './flags/salesforceId'; diff --git a/src/stubUx.ts b/src/stubUx.ts new file mode 100644 index 000000000..d515c7294 --- /dev/null +++ b/src/stubUx.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { SinonSandbox } from 'sinon'; +import { SfCommand } from './sfCommand'; +import { Spinner, Ux } from './ux'; + +export function stubUx(sandbox: SinonSandbox) { + return { + log: sandbox.stub(Ux.prototype, 'log'), + warn: sandbox.stub(Ux.prototype, 'warn'), + table: sandbox.stub(Ux.prototype, 'table'), + url: sandbox.stub(Ux.prototype, 'url'), + styledHeader: sandbox.stub(Ux.prototype, 'styledHeader'), + styledObject: sandbox.stub(Ux.prototype, 'styledObject'), + styledJSON: sandbox.stub(Ux.prototype, 'styledJSON'), + } +} + +export function stubSfCommandUx(sandbox: SinonSandbox) { + return { + log: sandbox.stub(SfCommand.prototype, 'log'), + logToStderr: sandbox.stub(SfCommand.prototype, 'logToStderr'), + logSuccess: sandbox.stub(SfCommand.prototype, 'logSuccess'), + logSensitive: sandbox.stub(SfCommand.prototype, 'logSensitive'), + info: sandbox.stub(SfCommand.prototype, 'info'), + warn: sandbox.stub(SfCommand.prototype, 'warn'), + table: sandbox.stub(SfCommand.prototype, 'table'), + url: sandbox.stub(SfCommand.prototype, 'url'), + styledHeader: sandbox.stub(SfCommand.prototype, 'styledHeader'), + styledObject: sandbox.stub(SfCommand.prototype, 'styledObject'), + styledJSON: sandbox.stub(SfCommand.prototype, 'styledJSON'), + } +} + +export function stubSpinner(sandbox: SinonSandbox) { + return { + start: sandbox.stub(Spinner.prototype, 'start'), + stop: sandbox.stub(Spinner.prototype, 'stop'), + } +} diff --git a/test/unit/stubUx.test.ts b/test/unit/stubUx.test.ts new file mode 100644 index 000000000..217a430d3 --- /dev/null +++ b/test/unit/stubUx.test.ts @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { createSandbox, SinonSandbox } from 'sinon'; +import { Interfaces } from '@oclif/core'; +import { expect } from 'chai'; +import { stubUx, stubSfCommandUx, SfCommand, Ux, stubSpinner, Flags } from '../../src/exported'; + +const TABLE_DATA = Array.from({ length: 10 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< + Record +>; +const TABLE_COLUMNS = { + id: { header: 'ID' }, + name: {}, + value: { header: 'TEST' }, +}; + +class Cmd extends SfCommand { + public static flags = { + method: Flags.custom<'SfCommand' | 'Ux'>({ + options: ['SfCommand', 'Ux'], + })({ + required: true, + }), + info: Flags.boolean(), + log: Flags.boolean(), + logSensitive: Flags.boolean(), + logSuccess: Flags.boolean(), + logToStderr: Flags.boolean(), + spinner: Flags.boolean(), + styledHeader: Flags.boolean(), + styledJSON: Flags.boolean(), + styledObject: Flags.boolean(), + table: Flags.boolean(), + url: Flags.boolean(), + warn: Flags.boolean(), + }; + + private flags!: Interfaces.InferredFlags; + + public async run(): Promise { + const { flags } = await this.parse(Cmd); + this.flags = flags; + + if (flags.info) this.runInfo(); + if (flags.log) this.runLog(); + if (flags.logSensitive) this.runLogSensitive(); + if (flags.logSuccess) this.runLogSuccess(); + if (flags.logToStderr) this.runLogToStderr(); + if (flags.spinner) this.runSpinner(); + if (flags.styledHeader) this.runStyledHeader(); + if (flags.styledJSON) this.runStyledJSON(); + if (flags.styledObject) this.runStyledObject(); + if (flags.table) this.runTable(); + if (flags.url) this.runUrl(); + if (flags.warn) this.runWarn(); + } + + private runInfo(): void { + switch (this.flags.method) { + case 'SfCommand': + this.info('hello'); + break; + case 'Ux': + throw new Error('Ux.info is not implemented'); + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + this.info('hello'); + } + + private runLog(): void { + switch (this.flags.method) { + case 'SfCommand': + this.log('hello'); + break; + case 'Ux': + new Ux().log('hello'); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runLogSuccess(): void { + switch (this.flags.method) { + case 'SfCommand': + this.logSuccess('hello'); + break; + case 'Ux': + throw new Error('Ux.logSuccess is not implemented'); + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runLogSensitive(): void { + switch (this.flags.method) { + case 'SfCommand': + this.logSensitive('hello'); + break; + case 'Ux': + throw new Error('Ux.logSensitive is not implemented'); + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runLogToStderr(): void { + switch (this.flags.method) { + case 'SfCommand': + this.logToStderr('hello'); + break; + case 'Ux': + throw new Error('Ux.logToStderr is not implemented'); + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runWarn(): void { + switch (this.flags.method) { + case 'SfCommand': + this.warn('hello'); + break; + case 'Ux': + new Ux().warn('hello'); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runTable(): void { + switch (this.flags.method) { + case 'SfCommand': + this.table(TABLE_DATA, TABLE_COLUMNS); + break; + case 'Ux': + new Ux().table(TABLE_DATA, TABLE_COLUMNS); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runUrl(): void { + switch (this.flags.method) { + case 'SfCommand': + this.url('oclif', 'https://oclif.io'); + break; + case 'Ux': + new Ux().url('oclif', 'https://oclif.io'); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runStyledHeader(): void { + switch (this.flags.method) { + case 'SfCommand': + this.styledHeader('hello'); + break; + case 'Ux': + new Ux().styledHeader('hello'); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runStyledObject(): void { + switch (this.flags.method) { + case 'SfCommand': + this.styledObject({ foo: 'bar' }); + break; + case 'Ux': + new Ux().styledObject({ foo: 'bar' }); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runStyledJSON(): void { + switch (this.flags.method) { + case 'SfCommand': + this.styledJSON({ foo: 'bar' }); + break; + case 'Ux': + new Ux().styledJSON({ foo: 'bar' }); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } + + private runSpinner(): void { + switch (this.flags.method) { + case 'SfCommand': + this.spinner.start('starting spinner'); + this.spinner.stop('done'); + break; + case 'Ux': + new Ux().spinner.start('starting spinner'); + new Ux().spinner.stop('done'); + break; + default: + throw new Error(`Invalid method: ${this.flags.method}`); + } + } +} + +describe('Ux Stubs', () => { + let uxStubs: ReturnType; + let sfCommandUxStubs: ReturnType; + let spinnerStubs: ReturnType; + let sandbox: SinonSandbox; + + beforeEach(() => { + sandbox = createSandbox(); + uxStubs = stubUx(sandbox); + sfCommandUxStubs = stubSfCommandUx(sandbox); + spinnerStubs = stubSpinner(sandbox); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('SfCommand methods', () => { + it('should stub log', async () => { + await Cmd.run(['--log', '--method=SfCommand']); + expect(sfCommandUxStubs.log.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub logSuccess', async () => { + await Cmd.run(['--logSuccess', '--method=SfCommand']); + expect(sfCommandUxStubs.logSuccess.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub logSensitive', async () => { + await Cmd.run(['--logSensitive', '--method=SfCommand']); + expect(sfCommandUxStubs.logSensitive.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub logToStderr', async () => { + await Cmd.run(['--logToStderr', '--method=SfCommand']); + expect(sfCommandUxStubs.logToStderr.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub warn', async () => { + await Cmd.run(['--warn', '--method=SfCommand']); + expect(sfCommandUxStubs.warn.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub table', async () => { + await Cmd.run(['--table', '--method=SfCommand']); + expect(sfCommandUxStubs.table.firstCall.args).to.deep.equal([TABLE_DATA, TABLE_COLUMNS]); + }); + + it('should stub url', async () => { + await Cmd.run(['--url', '--method=SfCommand']); + expect(sfCommandUxStubs.url.firstCall.args).to.deep.equal(['oclif', 'https://oclif.io']); + }); + + it('should stub styledHeader', async () => { + await Cmd.run(['--styledHeader', '--method=SfCommand']); + expect(sfCommandUxStubs.styledHeader.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub styledObject', async () => { + await Cmd.run(['--styledObject', '--method=SfCommand']); + expect(sfCommandUxStubs.styledObject.firstCall.args).to.deep.equal([{ foo: 'bar' }]); + }); + + it('should stub styledJSON', async () => { + await Cmd.run(['--styledJSON', '--method=SfCommand']); + expect(sfCommandUxStubs.styledJSON.firstCall.args).to.deep.equal([{ foo: 'bar' }]); + }); + + it('should stub spinner', async () => { + await Cmd.run(['--spinner', '--method=SfCommand']); + expect(true).to.be.true + expect(spinnerStubs.start.firstCall.args).to.deep.equal(['starting spinner']); + expect(spinnerStubs.stop.firstCall.args).to.deep.equal(['done']); + }); + }); + + describe('Ux methods run in SfCommand', () => { + it('should stub log', async () => { + await Cmd.run(['--log', '--method=Ux']); + expect(uxStubs.log.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub warn', async () => { + await Cmd.run(['--warn', '--method=Ux']); + expect(uxStubs.warn.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub table', async () => { + await Cmd.run(['--table', '--method=Ux']); + expect(uxStubs.table.firstCall.args).to.deep.equal([TABLE_DATA, TABLE_COLUMNS]); + }); + + it('should stub url', async () => { + await Cmd.run(['--url', '--method=Ux']); + expect(uxStubs.url.firstCall.args).to.deep.equal(['oclif', 'https://oclif.io']); + }); + + it('should stub styledHeader', async () => { + await Cmd.run(['--styledHeader', '--method=Ux']); + expect(uxStubs.styledHeader.firstCall.args).to.deep.equal(['hello']); + }); + + it('should stub styledObject', async () => { + await Cmd.run(['--styledObject', '--method=Ux']); + expect(uxStubs.styledObject.firstCall.args).to.deep.equal([{ foo: 'bar' }]); + }); + + it('should stub styledJSON', async () => { + await Cmd.run(['--styledJSON', '--method=Ux']); + expect(uxStubs.styledJSON.firstCall.args).to.deep.equal([{ foo: 'bar' }]); + }); + + it('should stub spinner', async () => { + await Cmd.run(['--spinner', '--method=Ux']); + expect(spinnerStubs.start.firstCall.args).to.deep.equal(['starting spinner']); + expect(spinnerStubs.stop.firstCall.args).to.deep.equal(['done']); + }); + }); +}); From d5add52a7414e6bb016cbb93b150d94d40d6abc3 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 17 Feb 2023 13:33:28 -0700 Subject: [PATCH 3/8] fix: use logJson --- src/sfCommand.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 87bcf1511..2b4c5164f 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -410,6 +410,10 @@ export abstract class SfCommand extends Command { }; } + protected logJson(json: AnyJson | unknown): void { + this.ux.styledJSON(json as AnyJson); + } + // eslint-disable-next-line class-methods-use-this protected async assignProject(): Promise { try { @@ -442,7 +446,7 @@ export abstract class SfCommand extends Command { }; if (this.jsonEnabled()) { - ux.styledJSON(this.toErrorJson(sfCommandError)); + this.logJson(this.toErrorJson(sfCommandError)); } else { this.logToStderr(this.formatError(sfCommandError)); } From 2b9489fecc15ea63a5e2a7455cf28783234b2a0e Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Fri, 17 Feb 2023 14:31:14 -0700 Subject: [PATCH 4/8] chore: test fixes --- test/unit/flags/apiVersion.test.ts | 2 +- test/unit/sfCommand.test.ts | 150 +++++++++++++++++------------ test/unit/stubUx.test.ts | 23 +++-- 3 files changed, 101 insertions(+), 74 deletions(-) diff --git a/test/unit/flags/apiVersion.test.ts b/test/unit/flags/apiVersion.test.ts index be662f547..7be44607c 100644 --- a/test/unit/flags/apiVersion.test.ts +++ b/test/unit/flags/apiVersion.test.ts @@ -20,10 +20,10 @@ const messages = Messages.loadMessages('@salesforce/sf-plugins-core', 'messages' describe('fs flags', () => { const sandbox = sinon.createSandbox(); - sandbox.stub(Lifecycle, 'getInstance').returns(Lifecycle.prototype); let warnStub: sinon.SinonStub; beforeEach(() => { + sandbox.stub(Lifecycle, 'getInstance').returns(Lifecycle.prototype); warnStub = sandbox.stub(Lifecycle.prototype, 'emitWarning'); }); diff --git a/test/unit/sfCommand.test.ts b/test/unit/sfCommand.test.ts index ced649e16..dfd8fda70 100644 --- a/test/unit/sfCommand.test.ts +++ b/test/unit/sfCommand.test.ts @@ -4,79 +4,103 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as os from 'os'; -import { test } from '@oclif/test'; -import { Config } from '@oclif/core'; +// import * as os from 'os'; +// import { test } from '@oclif/test'; +import { Flags } from '@oclif/core'; +import { Lifecycle } from '@salesforce/core'; +import { TestContext } from '@salesforce/core/lib/testSetup'; +import { stubMethod } from '@salesforce/ts-sinon'; import { expect } from 'chai'; import { SfError } from '@salesforce/core'; import { SfCommand } from '../../src/sfCommand'; -class TestCommand extends SfCommand { - // eslint-disable-next-line class-methods-use-this - public async run(): Promise { - return Promise.resolve(''); +class TestCommand extends SfCommand { + public static readonly flags = { + actions: Flags.boolean({ char: 'a', description: 'show actions' }), + error: Flags.boolean({ char: 'e', description: 'throw an error' }), + warn: Flags.boolean({ char: 'w', description: 'throw a warning' }), + }; + + public async run(): Promise { + const { flags } = await this.parse(TestCommand); + + if (flags.error && !flags.warn) { + const infoError = new SfError('foo bar baz', 'FooError', flags.actions ? ['this', 'is an', 'action'] : null); + this.info(infoError); + } else if (flags.warn) { + if (flags.error) { + const warnError = new SfError('foo bar baz', 'FooError', flags.actions ? ['this', 'is an', 'action'] : null); + this.warn(warnError); + } else { + this.warn('foo bar baz'); + } + } else { + this.info('foo bar baz'); + } } } describe('info messages', () => { - test - .stdout() - .do(() => { - const testCommand = new TestCommand([], {} as Config); - testCommand.info('foo bar baz'); - }) - .it('should show a info message from a string', (ctx) => { - expect(ctx.stdout).to.include('foo bar baz'); - }); - test - .stdout() - .do(() => { - const testCommand = new TestCommand([], {} as Config); - testCommand.info(new Error('foo bar baz') as SfCommand.Info); - }) - .it('should show a info message from Error, no actions', (ctx) => { - expect(ctx.stdout).to.include('foo bar baz'); - }); - test - .stdout() - .do(() => { - const testCommand = new TestCommand([], {} as Config); - const infoError = new SfError('foo bar baz', 'foo', ['this', 'is an', 'action']) as Error; - testCommand.info(infoError as SfCommand.Info); - }) - .it('should show a info message, with actions', (ctx) => { - expect(ctx.stdout).to.include('foo bar baz'); - expect(ctx.stdout).to.include(['this', 'is an', 'action'].join(os.EOL)); + const $$ = new TestContext(); + beforeEach(() => { + stubMethod($$.SANDBOX, Lifecycle, 'getInstance').returns({ + on: $$.SANDBOX.stub(), + onWarning: $$.SANDBOX.stub(), }); + }); + + it('should show a info message from a string', async () => { + const infoStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'info'); + await TestCommand.run([]); + expect(infoStub.calledWith('foo bar baz')).to.be.true; + }); + + it('should show a info message from Error, no actions', async () => { + const logStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'log'); + await TestCommand.run(['--error']); + expect(logStub.firstCall.firstArg).to.include('foo bar baz'); + }); + + it('should show a info message, with actions', async () => { + const logStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'log'); + await TestCommand.run(['--error', '--actions']); + expect(logStub.firstCall.firstArg) + .to.include('foo bar baz') + .and.to.include('this') + .and.to.include('is an') + .and.to.include('action'); + }); }); + describe('warning messages', () => { - test - .stderr() - .do(() => { - const testCommand = new TestCommand([], {} as Config); - testCommand.warn('foo bar baz'); - }) - .it('should show a info message from a string', (ctx) => { - expect(ctx.stderr).to.include('Warning: foo bar baz'); - }); - test - .stderr() - .do(() => { - const testCommand = new TestCommand([], {} as Config); - testCommand.warn(new Error('foo bar baz') as SfCommand.Warning); - }) - .it('should show a warning message from Error, no actions', (ctx) => { - expect(ctx.stderr).to.include('Warning: foo bar baz'); - }); - test - .stderr() - .do(() => { - const testCommand = new TestCommand([], {} as Config); - const infoError = new SfError('foo bar baz', 'foo', ['this', 'is an', 'action']) as Error; - testCommand.warn(infoError as SfCommand.Info); - }) - .it('should show a info message from Error, with actions', (ctx) => { - expect(ctx.stderr).to.include('Warning: foo bar baz'); - expect(ctx.stderr).to.include(['this', 'is an', 'action'].join(os.EOL)); + const $$ = new TestContext(); + beforeEach(() => { + stubMethod($$.SANDBOX, Lifecycle, 'getInstance').returns({ + on: $$.SANDBOX.stub(), + onWarning: $$.SANDBOX.stub(), }); + }); + + it('should show a info message from a string', async () => { + const logToStderrStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'logToStderr'); + await TestCommand.run(['--warn']); + expect(logToStderrStub.firstCall.firstArg).to.include('Warning').and.to.include('foo bar baz'); + }); + + it('should show a warning message from Error, no actions', async () => { + const logToStderrStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'logToStderr'); + await TestCommand.run(['--warn', '--error']); + expect(logToStderrStub.firstCall.firstArg).to.include('Warning').and.to.include('foo bar baz'); + }); + + it('should show a info message from Error, with actions', async () => { + const logToStderrStub = stubMethod($$.SANDBOX, SfCommand.prototype, 'logToStderr'); + await TestCommand.run(['--warn', '--error', '--actions']); + expect(logToStderrStub.firstCall.firstArg) + .to.include('Warning') + .and.to.include('foo bar baz') + .and.to.include('this') + .and.to.include('is an') + .and.to.include('action'); + }); }); diff --git a/test/unit/stubUx.test.ts b/test/unit/stubUx.test.ts index 217a430d3..d4fdc01aa 100644 --- a/test/unit/stubUx.test.ts +++ b/test/unit/stubUx.test.ts @@ -4,9 +4,11 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { createSandbox, SinonSandbox } from 'sinon'; import { Interfaces } from '@oclif/core'; import { expect } from 'chai'; +import { TestContext } from '@salesforce/core/lib/testSetup'; +import { stubMethod } from '@salesforce/ts-sinon'; +import { Lifecycle } from '@salesforce/core'; import { stubUx, stubSfCommandUx, SfCommand, Ux, stubSpinner, Flags } from '../../src/exported'; const TABLE_DATA = Array.from({ length: 10 }).fill({ id: '123', name: 'foo', value: 'bar' }) as Array< @@ -219,17 +221,18 @@ describe('Ux Stubs', () => { let uxStubs: ReturnType; let sfCommandUxStubs: ReturnType; let spinnerStubs: ReturnType; - let sandbox: SinonSandbox; + + const $$ = new TestContext(); beforeEach(() => { - sandbox = createSandbox(); - uxStubs = stubUx(sandbox); - sfCommandUxStubs = stubSfCommandUx(sandbox); - spinnerStubs = stubSpinner(sandbox); - }); + stubMethod($$.SANDBOX, Lifecycle, 'getInstance').returns({ + on: $$.SANDBOX.stub(), + onWarning: $$.SANDBOX.stub(), + }); - afterEach(() => { - sandbox.restore(); + uxStubs = stubUx($$.SANDBOX); + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); + spinnerStubs = stubSpinner($$.SANDBOX); }); describe('SfCommand methods', () => { @@ -285,7 +288,7 @@ describe('Ux Stubs', () => { it('should stub spinner', async () => { await Cmd.run(['--spinner', '--method=SfCommand']); - expect(true).to.be.true + expect(true).to.be.true; expect(spinnerStubs.start.firstCall.args).to.deep.equal(['starting spinner']); expect(spinnerStubs.stop.firstCall.args).to.deep.equal(['done']); }); From 561aeca7dac6c4677a176ef3b3fe2eadfe735178 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 20 Feb 2023 10:10:44 -0700 Subject: [PATCH 5/8] fix: add prompter stubs --- package.json | 2 +- src/stubUx.ts | 16 ++++++++++---- test/unit/sfCommand.test.ts | 2 -- yarn.lock | 43 +++++++++++++++++++++++++++++++------ 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 708832b48..f8e3d60e6 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "/messages" ], "dependencies": { - "@oclif/core": "^2.1.1", + "@oclif/core": "^2.2.0", "@salesforce/core": "^3.33.1", "@salesforce/kit": "^1.8.3", "@salesforce/ts-types": "^1.7.1", diff --git a/src/stubUx.ts b/src/stubUx.ts index d515c7294..73f110876 100644 --- a/src/stubUx.ts +++ b/src/stubUx.ts @@ -7,7 +7,7 @@ import { SinonSandbox } from 'sinon'; import { SfCommand } from './sfCommand'; -import { Spinner, Ux } from './ux'; +import { Prompter, Spinner, Ux } from './ux'; export function stubUx(sandbox: SinonSandbox) { return { @@ -18,7 +18,7 @@ export function stubUx(sandbox: SinonSandbox) { styledHeader: sandbox.stub(Ux.prototype, 'styledHeader'), styledObject: sandbox.stub(Ux.prototype, 'styledObject'), styledJSON: sandbox.stub(Ux.prototype, 'styledJSON'), - } + }; } export function stubSfCommandUx(sandbox: SinonSandbox) { @@ -34,12 +34,20 @@ export function stubSfCommandUx(sandbox: SinonSandbox) { styledHeader: sandbox.stub(SfCommand.prototype, 'styledHeader'), styledObject: sandbox.stub(SfCommand.prototype, 'styledObject'), styledJSON: sandbox.stub(SfCommand.prototype, 'styledJSON'), - } + }; } export function stubSpinner(sandbox: SinonSandbox) { return { start: sandbox.stub(Spinner.prototype, 'start'), stop: sandbox.stub(Spinner.prototype, 'stop'), - } + }; +} + +export function stubPrompter(sandbox: SinonSandbox) { + return { + prompt: sandbox.stub(Prompter.prototype, 'prompt'), + confirm: sandbox.stub(Prompter.prototype, 'confirm'), + timedPrompt: sandbox.stub(Prompter.prototype, 'timedPrompt'), + }; } diff --git a/test/unit/sfCommand.test.ts b/test/unit/sfCommand.test.ts index dfd8fda70..cbf41bed7 100644 --- a/test/unit/sfCommand.test.ts +++ b/test/unit/sfCommand.test.ts @@ -4,8 +4,6 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -// import * as os from 'os'; -// import { test } from '@oclif/test'; import { Flags } from '@oclif/core'; import { Lifecycle } from '@salesforce/core'; import { TestContext } from '@salesforce/core/lib/testSetup'; diff --git a/yarn.lock b/yarn.lock index 4ff6a523f..6b284f066 100644 --- a/yarn.lock +++ b/yarn.lock @@ -574,10 +574,10 @@ widest-line "^3.1.0" wrap-ansi "^7.0.0" -"@oclif/core@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.1.1.tgz#8ffa83fe39f43bd66fb6f0731527b17911fc937e" - integrity sha512-t0zez9ydn3eveZM2vsMbrL2hNKzlHSfwEl4A29VlIimOxpQDB9AKZXX02lhTB3BSKbVF24UOlkzYiX7n9Bdc0A== +"@oclif/core@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.2.0.tgz#8f8d21df8a664484da4632ae253947a535f7bb2f" + integrity sha512-gA1gSZFfVqLWkwdw5qqCK4a0nuwrm1aGCpgNijG1PiNjcro5dD1YxGSooIkxlqqZsXTyj7GJYjemwWwLBQK6cw== dependencies: "@types/cli-progress" "^3.11.0" ansi-escapes "^4.3.2" @@ -585,9 +585,9 @@ cardinal "^2.1.1" chalk "^4.1.2" clean-stack "^3.0.1" - cli-progress "^3.10.0" + cli-progress "^3.11.2" debug "^4.3.4" - ejs "^3.1.6" + ejs "^3.1.8" fs-extra "^9.1.0" get-package-type "^0.1.0" globby "^11.1.0" @@ -1399,6 +1399,11 @@ async@^3.2.0: resolved "https://registry.npmjs.org/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1606,7 +1611,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@^4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1688,6 +1693,13 @@ cli-progress@^3.10.0, cli-progress@^3.4.0: dependencies: string-width "^4.2.0" +cli-progress@^3.11.2: + version "3.12.0" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942" + integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A== + dependencies: + string-width "^4.2.3" + cli-spinners@^2.5.0: version "2.6.0" resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" @@ -2148,6 +2160,13 @@ ejs@^3.1.6: dependencies: jake "^10.6.1" +ejs@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.3.723: version "1.3.756" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.756.tgz#942cee59cd64d19f576d8d5804eef09cb423740c" @@ -3626,6 +3645,16 @@ jake@^10.6.1: filelist "^1.0.1" minimatch "^3.0.4" +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + js-sdsl@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" From d1421153090d93a2606f06659ea5501a1ba3dfa4 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 20 Feb 2023 10:18:42 -0700 Subject: [PATCH 6/8] chore: remove ts-types for external nuts --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 196510485..2429abffe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/plugin-deploy-retrieve' - preBuildCommands: 'shx rm -rf node_modules/@oclif/core; shx rm -rf node_modules/@salesforce/kit; shx rm -rf node_modules/@salesforce/core' + preBuildCommands: 'shx rm -rf node_modules/@oclif/core; shx rm -rf node_modules/@salesforce/kit; shx rm -rf node_modules/@salesforce/core; shx rm -rf node_modules/@salesforce/ts-types' command: ${{ matrix.command }} os: ${{ matrix.os }} secrets: inherit @@ -69,7 +69,7 @@ jobs: with: packageName: '@salesforce/sf-plugins-core' externalProjectGitUrl: 'https://github.com/salesforcecli/${{matrix.repo}}' - preBuildCommands: 'shx rm -rf node_modules/@oclif/core ; shx rm -rf node_modules/@salesforce/kit ; shx rm -rf node_modules/@salesforce/core' + preBuildCommands: 'shx rm -rf node_modules/@oclif/core; shx rm -rf node_modules/@salesforce/kit; shx rm -rf node_modules/@salesforce/core; shx rm -rf node_modules/@salesforce/ts-types' command: yarn test:nuts os: ${{ matrix.os }} secrets: inherit From 2bc6509007731bee11befe4a47997e63268f11d9 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 20 Feb 2023 11:35:40 -0700 Subject: [PATCH 7/8] chore: revert changelog From 826eefa3f5c8fcef9ddd1a72e23f32aeaea7741d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 20 Feb 2023 13:13:02 -0700 Subject: [PATCH 8/8] chore: fix logJson --- src/sfCommand.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sfCommand.ts b/src/sfCommand.ts index 2b4c5164f..c07a2b1da 100644 --- a/src/sfCommand.ts +++ b/src/sfCommand.ts @@ -410,8 +410,12 @@ export abstract class SfCommand extends Command { }; } + // eslint-disable-next-line class-methods-use-this protected logJson(json: AnyJson | unknown): void { - this.ux.styledJSON(json as AnyJson); + // If `--json` is enabled, then the ux instance on the class will disable output, which + // means that the logJson method will not output anything. So, we need to create a new + // instance of the ux class that does not have output disabled in order to log the json. + new Ux().styledJSON(json as AnyJson); } // eslint-disable-next-line class-methods-use-this