From 2f4f43a59173eae8608b859b90db5d48a439c8b1 Mon Sep 17 00:00:00 2001 From: Bastian Gebhardt Date: Fri, 13 Aug 2021 22:42:44 +0200 Subject: [PATCH] Add dynamic text for logger.success and logger.error --- constants.ts | 2 +- interfaces.ts | 6 +- loggerSpinner.ts | 33 ++-- store/loggerSpinner.spec.ts | 318 ++++++++++++++++++++++++++++++++++++ store/readStore.spec.ts | 2 +- store/readStore.ts | 11 +- utils.spec.ts | 16 +- utils.ts | 6 +- 8 files changed, 373 insertions(+), 21 deletions(-) create mode 100644 store/loggerSpinner.spec.ts diff --git a/constants.ts b/constants.ts index e154427..869836e 100644 --- a/constants.ts +++ b/constants.ts @@ -9,7 +9,7 @@ export const LOAD_CONFIG: LoggerConfig = { export const READ_CONFIG: LoggerConfig = { start: 'Reading local storage file from filesystem', success: 'Successfully loaded localstore.json from filesystem', - fail: 'Error loading localstore.json from filesystem', + fail: reason => `Error loading localstore.json from filesystem: ${reason}`, } export const LOCAL_STORE_FILE = 'localstore.json' diff --git a/interfaces.ts b/interfaces.ts index 26a4026..452b6bb 100644 --- a/interfaces.ts +++ b/interfaces.ts @@ -78,8 +78,8 @@ export interface IMetadataResponse { export interface LoggerConfig { start: string - success: string - fail: string + success: string | TextFunction + fail: string | TextFunction } export type Store = { @@ -92,3 +92,5 @@ export type Store = { export type StoreSize = { [p in OS]: number } + +export type TextFunction = (key: string) => string diff --git a/loggerSpinner.ts b/loggerSpinner.ts index 42adf36..dd80267 100644 --- a/loggerSpinner.ts +++ b/loggerSpinner.ts @@ -1,6 +1,7 @@ import * as chalk from 'chalk' -import { LoggerConfig } from './interfaces' +import { LoggerConfig, TextFunction } from './interfaces' +import { isTextFunction } from './utils' export class LoggerSpinner { @@ -11,20 +12,20 @@ export class LoggerSpinner { private readonly WARN_FN = (msg: string) => chalk.yellow(`! ${msg}`) private startText: string | undefined - private successText: string | undefined - private errorText: string | undefined + private successText: string | undefined | TextFunction + private errorText: string | undefined | TextFunction private stdio: NodeJS.WriteStream private timer: ReturnType | null = null - public constructor() { - this.stdio = process.stdout + public constructor(stdio: NodeJS.WriteStream) { + this.stdio = stdio } private clearLine(): LoggerSpinner { try { this.stdio.clearLine(0) this.stdio.cursorTo(0) - } catch { + } catch(e) { // this might fail when piping stdout to /dev/null. Just ignore it in this case } return this @@ -51,8 +52,12 @@ export class LoggerSpinner { public start(loggingConfig: LoggerConfig): LoggerSpinner { const { start, success, fail } = loggingConfig this.startText = start - this.successText = this.SUCCESS_FN(success) - this.errorText = this.ERROR_FN(fail || this.DEFAULT_ERROR) + this.successText = isTextFunction(success) + ? (text: string) => this.SUCCESS_FN(success(text)) + : this.SUCCESS_FN(success) + this.errorText = isTextFunction(fail) + ? (text: string) => this.ERROR_FN(fail(text)) + : this.ERROR_FN(fail) this.stop() let count = 0 @@ -82,19 +87,23 @@ export class LoggerSpinner { .newline() } - public success(): LoggerSpinner { + public success(text?: string): LoggerSpinner { return this.clearLine() .stop() - .write(this.successText || '') + .write(isTextFunction(this.successText) + ? this.successText(text || '') + : this.successText || '') .newline() } public error(text?: string): LoggerSpinner { return this.clearLine() .stop() - .write(text ? this.ERROR_FN(text) : this.errorText || '') + .write(isTextFunction(this.errorText) + ? this.errorText(text || '') + : this.errorText || '') .newline() } } -export const logger = new LoggerSpinner() +export const logger = new LoggerSpinner(process.stdout) diff --git a/store/loggerSpinner.spec.ts b/store/loggerSpinner.spec.ts new file mode 100644 index 0000000..925aaae --- /dev/null +++ b/store/loggerSpinner.spec.ts @@ -0,0 +1,318 @@ +import * as chalk from 'chalk' +import { MaybeMockedDeep } from 'ts-jest/dist/utils/testing' +import { mocked } from 'ts-jest/utils' +import { LoggerSpinner } from '../loggerSpinner' + +jest.mock('chalk', () => ({ + green: (text: string) => `green: ${text}`, + red: (text: string) => `red: ${text}`, + +})) + +interface PartialStdio { + write: () => boolean + clearLine: () => boolean + cursorTo: () => boolean +} + +describe('loggerSpinner', () => { + + let logger: LoggerSpinner + let stdioMock: MaybeMockedDeep + + beforeAll(() => { + stdioMock = { + write: jest.fn(), + clearLine: jest.fn(), + cursorTo: jest.fn(), + } + + logger = new LoggerSpinner(stdioMock as unknown as NodeJS.WriteStream) + }) + + beforeEach(() => { + jest.useFakeTimers() + + mocked(chalk, true) + + stdioMock.write.mockReset() + stdioMock.clearLine.mockReset() + stdioMock.cursorTo.mockReset() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + describe('start', () => { + it('should write the initial spinner including text', () => { + expect(stdioMock.write).toHaveBeenCalledTimes(0) + + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + expect(stdioMock.write).toHaveBeenCalledWith('⠏ start_text') + expect(stdioMock.clearLine).toBeCalledTimes(0) + expect(stdioMock.cursorTo).toBeCalledTimes(0) + }) + + it('should write the spinner after one tick', () => { + expect(stdioMock.write).toHaveBeenCalledTimes(0) + + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + jest.advanceTimersByTime(100) + + expect(stdioMock.write).toHaveBeenCalledTimes(2) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['⠋ start_text'], + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should surpress if clearLine throws an error', () => { + stdioMock.clearLine.mockImplementation(() => { + throw new Error() + }) + + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + jest.advanceTimersByTime(100) + + expect(stdioMock.write).toHaveBeenCalledTimes(2) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['⠋ start_text'], + ]) + expect(stdioMock.clearLine).toHaveBeenCalledTimes(1) + expect(stdioMock.cursorTo).toHaveBeenCalledTimes(0) + }) + + it('should surpress if cursorTo throws an error', () => { + stdioMock.cursorTo.mockImplementation(() => { + throw new Error() + }) + + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + jest.advanceTimersByTime(100) + + expect(stdioMock.write).toHaveBeenCalledTimes(2) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['⠋ start_text'], + ]) + expect(stdioMock.clearLine).toHaveBeenCalledTimes(1) + expect(stdioMock.cursorTo).toHaveBeenCalledTimes(1) + }) + }) + + describe('success', () => { + it('should write the success text', () => { + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.success() + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['green: ✔ success_text'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should write the success text with dynamic text', () => { + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: text => `success_text (${text})`, + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.success('foo') + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['green: ✔ success_text (foo)'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should ignore the dynamic text on no TextFunction passed', () => { + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.success('foo') + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['green: ✔ success_text'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should print the success text without dynamic text', () => { + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: text => `success_text (${text})`, + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.success() + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['green: ✔ success_text ()'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + }) + + describe('error', () => { + it('should write the fail text', () => { + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.error() + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['red: ✘ fail_text'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should write the fail text with dynamic text', () => { + logger.start({ + start: 'start_text', + fail: text => `fail_text (${text})`, + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.error('foo') + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['red: ✘ fail_text (foo)'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should ignore the dynamic text on no TextFunction passed', () => { + logger.start({ + start: 'start_text', + fail: 'fail_text', + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.error('foo') + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['red: ✘ fail_text'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + + it('should print the fail text without dynamic text', () => { + logger.start({ + start: 'start_text', + fail: text => `fail_text (${text})`, + success: 'success_text', + }) + + expect(stdioMock.write).toHaveBeenCalledTimes(1) + + logger.error() + + expect(stdioMock.write).toHaveBeenCalledTimes(3) + expect(stdioMock.write.mock.calls).toEqual([ + ['⠏ start_text'], + ['red: ✘ fail_text ()'], + ['\n'] + ]) + expect(stdioMock.clearLine).toBeCalledTimes(1) + expect(stdioMock.clearLine).toBeCalledWith(0) + expect(stdioMock.cursorTo).toBeCalledTimes(1) + expect(stdioMock.cursorTo).toBeCalledWith(0) + }) + }) +}) diff --git a/store/readStore.spec.ts b/store/readStore.spec.ts index 09b1458..041a1be 100644 --- a/store/readStore.spec.ts +++ b/store/readStore.spec.ts @@ -58,7 +58,7 @@ describe('readStore', () => { await readStoreFile(config) fail() } catch (e) { - expect(e).toEqual(new Error('File does not exist!')) + expect(e).toEqual(new Error('File does not exist')) expect(fsMock.readFile).toHaveBeenCalledTimes(0) } }) diff --git a/store/readStore.ts b/store/readStore.ts index f78074d..95a411a 100644 --- a/store/readStore.ts +++ b/store/readStore.ts @@ -13,8 +13,9 @@ const readFilePromise = promisify(readFile) export async function readStoreFile(config: IStoreConfig): Promise { logger.start(READ_CONFIG) if (!existsSync(config.url)) { - logger.error() - throw new Error('File does not exist!') + const reason = 'File does not exist' + logger.error(reason) + throw new Error(reason) } try { @@ -22,7 +23,11 @@ export async function readStoreFile(config: IStoreConfig): Promise { logger.success() return JSON.parse(store) } catch(e) { - logger.error() + if (e instanceof SyntaxError) { + logger.error('Unable to parse JSON file') + } else { + logger.error(e.toString()) + } throw e } } diff --git a/utils.spec.ts b/utils.spec.ts index 063eb49..646988d 100644 --- a/utils.spec.ts +++ b/utils.spec.ts @@ -1,7 +1,7 @@ import { MaybeMockedDeep } from 'ts-jest/dist/utils/testing' import { mocked } from 'ts-jest/utils' -import { detectOperatingSystem, sortDescendingIMappedVersions, compareComparableVersions, sortAscendingIMappedVersions, sortStoreEntries } from './utils' +import { detectOperatingSystem, sortDescendingIMappedVersions, compareComparableVersions, sortAscendingIMappedVersions, sortStoreEntries, isTextFunction } from './utils' import { logger, LoggerSpinner } from './loggerSpinner' import { createChromeConfig, createStore } from './test.utils' import { IMappedVersion, Compared } from './interfaces' @@ -313,4 +313,18 @@ describe('utils', () => { expect(sortStoreEntries(unsortedAll)).toEqual(sortedAll) }) }) + + describe('isTextFunction', () => { + it('should correctly identify a string', () => { + expect(isTextFunction('foo')).toBe(false) + }) + + it('should correctly a TextFunction', () => { + expect(isTextFunction(text => `text is: ${text}`)).toBe(true) + }) + + it('should correctly identify undefined', () => { + expect(isTextFunction(undefined)).toBe(false) + }) + }) }) diff --git a/utils.ts b/utils.ts index 7459191..41c8b7c 100644 --- a/utils.ts +++ b/utils.ts @@ -1,4 +1,4 @@ -import { ExtendedOS, OS, IChromeConfig, IMappedVersion, Compared, Store } from './interfaces' +import { ExtendedOS, OS, IChromeConfig, IMappedVersion, Compared, Store, TextFunction } from './interfaces' import { logger } from './loggerSpinner' import { ComparableVersion } from './commons/ComparableVersion' @@ -118,3 +118,7 @@ export function sortStoreEntries(store: Store): Store { }, } } + +export function isTextFunction(value: string | TextFunction | undefined): value is TextFunction { + return typeof value === 'function' +}