diff --git a/__mocks__/@octokit/rest.ts b/__mocks__/@octokit/rest.ts index efecdb0..63b934c 100644 --- a/__mocks__/@octokit/rest.ts +++ b/__mocks__/@octokit/rest.ts @@ -1,6 +1,10 @@ -// tslint:disable:no-any no-unsafe-any +// tslint:disable:no-any no-unsafe-any no-magic-numbers jest.genMockFromModule('@octokit/rest'); +const gistId = Math.random() + .toString(36) + .slice(7); + const gistsResponseData = [ { created_at: new Date().toString(), @@ -54,6 +58,18 @@ const gistsResponse = (): Promise => }); const mockedGists = { + create: jest.fn((params) => + Promise.resolve({ + data: { + created_at: new Date().toString(), + description: params.description, + files: params.files, + id: gistId, + public: params.public, + updated_at: new Date().toString() + } + }) + ), get: jest.fn((options) => Promise.resolve({ data: { ...gistsResponseData[0], id: options.gist_id } diff --git a/jest.config.js b/jest.config.js index 8a89e78..bc7a822 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ module.exports = { testMatch: ['**/__tests__/**/(*.)+(spec|test).ts?(x)'], coverageThreshold: { global: { - branches: 60, // TODO: adjust this back to 80% + branches: 70, // TODO: adjust this back to 80% functions: 80, lines: 80, statements: -30 // TODO: adjust this back to 10 diff --git a/package.json b/package.json index 9d4756e..4c167a1 100644 --- a/package.json +++ b/package.json @@ -19,53 +19,53 @@ "commands": [ { "command": "extension.openCodeBlock", - "title": "GIST: Open Block", - "category": "Gist" + "title": "Open Block", + "category": "GIST" }, { "command": "extension.openFavoriteCodeBlock", - "title": "GIST: Open Favorite Block", - "category": "Gist" + "title": "Open Favorite Block", + "category": "GIST" }, { "command": "extension.createCodeBlock", - "title": "GIST: Create New Block", - "category": "Gist" + "title": "Create New Block", + "category": "GIST" }, { "command": "extension.openCodeBlockInBrowser", - "title": "GIST: Open Block In Browser", - "category": "Gist" + "title": "Open Block In Browser", + "category": "GIST" }, { "command": "extension.deleteCodeBlock", - "title": "GIST: Delete Block", - "category": "Gist" + "title": "Delete Block", + "category": "GIST" }, { "command": "extension.removeFileFromCodeBlock", - "title": "GIST: Remove From Block", - "category": "Gist" + "title": "Remove From Block", + "category": "GIST" }, { "command": "extension.addToCodeBlock", - "title": "GIST: Add To Block", - "category": "Gist" + "title": "Add To Block", + "category": "GIST" }, { "command": "extension.changeCodeBlockDescription", - "title": "GIST: Change Block Description", - "category": "Gist" + "title": "Change Block Description", + "category": "GIST" }, { "command": "extension.insertCode", - "title": "GIST: Insert Code Into Current File", - "category": "Gist" + "title": "Insert Code Into Current File", + "category": "GIST" }, { "command": "extension.toggleProfile", - "title": "GIST: Toggle Profile", - "category": "Gist" + "title": "Toggle Profile", + "category": "GIST" } ], "keybindings": [ diff --git a/src/commands/__tests__/gists.commands.test.ts b/src/commands/__tests__/gists.commands.test.ts index 75fbcc4..f7d9666 100644 --- a/src/commands/__tests__/gists.commands.test.ts +++ b/src/commands/__tests__/gists.commands.test.ts @@ -6,6 +6,7 @@ import { gists } from '../../gists/gists-service'; import { logger } from '../../logger'; import { profiles } from '../../profiles'; import { + createCodeBlock, openCodeBlock, openFavoriteCodeBlock, updateCodeBlock, @@ -14,6 +15,8 @@ import { jest.mock('fs'); jest.mock('path'); +const promptMock: any = jest.genMockFromModule('../../utils/prompt'); +jest.mock('../../utils/prompt', promptMock); const executeCommandSpy = jest.spyOn(commands, 'executeCommand'); const configureSpy = jest.spyOn(gists, 'configure'); @@ -132,4 +135,32 @@ describe('Gists Commands Tests', () => { }); }); }); + describe('#createCodeBlock', () => { + test('should create a code block and open the files', async () => { + expect.assertions(2); + + promptMock.prompt.mockImplementation( + (_msg: string, defaultValue: string) => Promise.resolve(defaultValue) + ); + + const codeBlock = { + fileName: `${TMP_DIRECTORY_PREFIX}_123456789abcdefg_random_string/test-file-name.md`, + getText: jest.fn(() => 'test-file-content') + }; + + const editor = { + document: codeBlock, + selection: { isEmpty: true } + }; + + (window).activeTextEditor = editor; + + await createCodeBlock(); + + expect(editor.document.getText.mock.calls).toHaveLength(2); + expect(executeCommandSpy.mock.calls[0][0]).toStrictEqual( + 'workbench.action.keepEditor' + ); + }); + }); }); diff --git a/src/commands/gists.commands.ts b/src/commands/gists.commands.ts index 88d9cad..22c302b 100644 --- a/src/commands/gists.commands.ts +++ b/src/commands/gists.commands.ts @@ -1,10 +1,15 @@ import { commands, window, workspace } from 'vscode'; -import { configure, getGist, getGists, updateGist } from '../gists'; +import { configure, createGist, getGist, getGists, updateGist } from '../gists'; import { insights } from '../insights'; import { logger } from '../logger'; import { profiles } from '../profiles'; -import { extractTextDocumentDetails, filesSync, notify } from '../utils'; +import { + extractTextDocumentDetails, + filesSync, + notify, + prompt +} from '../utils'; const _formatGistsForQuickPick = (gists: Gist[]): QuickPickGist[] => gists.map((item, i, j) => ({ @@ -27,6 +32,24 @@ const _openDocument = async (file: string): Promise => { commands.executeCommand('workbench.action.keepEditor'); }; +const _openCodeBlock = async ( + gistId: string +): Promise<{ + fileCount: number; + files: { [x: string]: { content: string } }; + id: string; +}> => { + const { id, files, fileCount } = await getGist(gistId); + const filePaths = filesSync(id, files); + + // await is not available not available in forEach + for (const filePath of filePaths) { + await _openDocument(filePath); + } + + return { id, files, fileCount }; +}; + const openCodeBlock = async (favorite = false): Promise => { let gistName = ''; try { @@ -41,13 +64,7 @@ const openCodeBlock = async (favorite = false): Promise => { gistName = `"${selected.block.name}"`; logger.info(`User Selected Gist: "${selected.label}"`); - const { id, files, fileCount } = await getGist(selected.block.id); - const filePaths = filesSync(id, files); - - // await is not available not available in forEach - for (const filePath of filePaths) { - await _openDocument(filePath); - } + const { fileCount } = await _openCodeBlock(selected.block.id); logger.info('Opened Gist'); insights.track('open', undefined, { @@ -105,7 +122,45 @@ const updateGistAccessKey = (): void => { insights.track('updateGistAccessKey', { url }); }; +const createCodeBlock = async (): Promise => { + let gistName = ''; + try { + const editor = window.activeTextEditor; + if (!editor) { + throw new Error('Open a file before creating'); + } + const selection = editor.selection; + const content = editor.document.getText( + selection.isEmpty ? undefined : selection + ); + const details = extractTextDocumentDetails(editor.document); + const filename = (details && details.filename) || 'untitled.txt'; + const description = await prompt('Enter description'); + const isPublic = + ((await prompt('Public? Y = Yes, N = No', 'Y')) || 'Y') // TODO: add configuration for default value + .slice(0, 1) + .toLowerCase() === 'y'; + + gistName = description || filename; + + const gist = await createGist( + { [filename]: { content } }, + description, + isPublic + ); + + await _openCodeBlock(gist.id); + } catch (err) { + const context = gistName ? ` ${gistName}` : ''; + const error: Error = err as Error; + logger.error(`createCodeBlock > ${error && error.message}`); + insights.exception('createCodeBlock', { messsage: error.message }); + notify.error(`Could Not Create${context}`, `Reason: ${error.message}`); + } +}; + export { + createCodeBlock, updateGistAccessKey, openCodeBlock, openFavoriteCodeBlock, diff --git a/src/extension.ts b/src/extension.ts index 10d1d0e..a4f0b05 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import { commands, ExtensionContext, workspace } from 'vscode'; import { + createCodeBlock, createProfile, openCodeBlock, openFavoriteCodeBlock, @@ -45,6 +46,7 @@ export function activate(context: ExtensionContext): void { 'extension.openFavoriteCodeBlock', openFavoriteCodeBlock ); + commands.registerCommand('extension.createCodeBlock', createCodeBlock); workspace.onDidSaveTextDocument(updateCodeBlock); commands.registerCommand( 'extension.updateGistAccessKey', @@ -78,12 +80,6 @@ export function activate(context: ExtensionContext): void { }); }); - commands.registerCommand( - 'extension.createCodeBlock', - (): void => { - // intentionally left blank - } - ); commands.registerCommand( 'extension.openCodeBlockInBrowser', (): void => { diff --git a/src/gists/__tests__/api.test.ts b/src/gists/__tests__/api.test.ts index 6dc487c..692a802 100644 --- a/src/gists/__tests__/api.test.ts +++ b/src/gists/__tests__/api.test.ts @@ -1,95 +1,12 @@ // tslint:disable:no-any no-magic-numbers no-unsafe-any -import * as Octokit from '@octokit/rest'; -const gistsResponseData = [ - { - created_at: new Date().toString(), - description: 'gist one', - files: { - 'one.md': { - filename: 'one.md', - language: 'markdown', - raw_url: 'https://foo.bar/api/test123/1', - size: '11111', - type: 'text' - }, - 'two.md': { - filename: 'two.md', - language: 'markdown', - raw_url: 'https://foo.bar/api/test123/2', - size: '22222', - type: 'text' - } - }, - id: 'test123', - updated_at: new Date().toString() - }, - { - created_at: new Date().toString(), - description: 'gist two', - files: { - 'one.md': { - filename: 'one.md', - language: 'markdown', - raw_url: 'https://foo.bar/api/test123/1', - size: '11111', - type: 'text' - }, - 'two.md': { - filename: 'two.md', - language: 'markdown', - raw_url: 'https://foo.bar/api/test123/2', - size: '22222', - type: 'text' - } - }, - id: 'test123', - updated_at: new Date().toString() - } -]; -const update = jest.fn((options) => - Promise.resolve({ - data: { - ...gistsResponseData[0], - files: { - ...gistsResponseData[0].files, - ...options.files - }, - id: options.gist_id || gistsResponseData[0].id - } - }) -); -const get = jest.fn((options) => - Promise.resolve({ - data: { ...gistsResponseData[0], id: options.gist_id } - }) -); -const list = jest.fn(() => Promise.resolve({ data: gistsResponseData })); -const listStarred = jest.fn(() => Promise.resolve({ data: gistsResponseData })); -jest.genMockFromModule('@octokit/rest'); -jest.mock('@octokit/rest'); -(Octokit as any).mockImplementation( - (): any => ({ gists: { update, get, list, listStarred } }) -); - -import { getGist, getGists, updateGist } from '../api'; +import { createGist, getGist, getGists, updateGist } from '../api'; describe('Gists API Tests', () => { afterAll(() => { jest.clearAllMocks(); }); describe('#getGists', () => { - test('handles error appropriately', async () => { - expect.assertions(1); - - list.mockRejectedValueOnce(new Error('Not Found')); - - try { - await getGists(); - } catch (err) { - expect(err).toStrictEqual(new Error('Not Found')); - } - }); test('list without params should return one block', async () => { expect.assertions(2); @@ -120,17 +37,6 @@ describe('Gists API Tests', () => { }); }); describe('#updateGist', () => { - test('handles error appropriately', async () => { - expect.assertions(1); - - update.mockRejectedValueOnce(new Error('Not Found')); - - try { - await updateGist('1', '2', '3'); - } catch (err) { - expect(err).toStrictEqual(new Error('Not Found')); - } - }); test('updates a gist', async () => { expect.assertions(2); @@ -148,4 +54,20 @@ describe('Gists API Tests', () => { }); }); }); + describe('#createGist', () => { + test('creates a gist', async () => { + expect.assertions(2); + + const gist = await createGist( + { 'file-one.txt': { content: 'test-content' } }, + 'test-description', + true + ); + + expect(typeof gist.id).toStrictEqual('string'); + expect(gist.files).toStrictEqual({ + 'file-one.txt': { content: 'test-content' } + }); + }); + }); }); diff --git a/src/gists/__tests__/gists-service.test.ts b/src/gists/__tests__/gists-service.test.ts index 7c49aa4..fd61c31 100644 --- a/src/gists/__tests__/gists-service.test.ts +++ b/src/gists/__tests__/gists-service.test.ts @@ -23,6 +23,22 @@ describe('GistService tests', () => { }); }); }); + describe('#create', () => { + test('should creat a gist', (done) => { + expect.assertions(1); + + testGists + .create({ + description: 'test', + files: { 'fileone.txt': { content: 'test content' } }, + public: true + }) + .then((response: any) => { + expect(response.data.description).toStrictEqual('test'); + done(); + }); + }); + }); describe('#list', () => { test('should have list function', (done) => { expect.assertions(1); diff --git a/src/gists/api.ts b/src/gists/api.ts index fbd136c..d2425f7 100644 --- a/src/gists/api.ts +++ b/src/gists/api.ts @@ -118,3 +118,23 @@ export const configure = (options: { key: string; url: string }): void => { const url = options.url || GISTS_BASE_URL; gists.configure({ key, url }); }; + +export const createGist = async ( + files: { [x: string]: { content: string } }, + description?: string, + isPublic = true +): Promise => { + try { + const results = await gists.create({ + description, + files, + public: isPublic + }); + + return formatGist(results.data); + } catch (err) { + const error: Error = prepareError(err as Error); + + throw error; + } +}; diff --git a/src/gists/gists-service.ts b/src/gists/gists-service.ts index a3495d6..04c047e 100644 --- a/src/gists/gists-service.ts +++ b/src/gists/gists-service.ts @@ -30,6 +30,12 @@ class GistsService { this.octokit.authenticate({ type: 'token', token: options.key }); } + public create( + params: Octokit.GistsCreateParams + ): Response { + return this.octokit.gists.create(params); + } + public get( params: Octokit.GistsGetParams ): Response { diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts index 7977295..60d4fbb 100644 --- a/src/typings/global.d.ts +++ b/src/typings/global.d.ts @@ -20,7 +20,7 @@ interface Gist { interface GistTextDocument { fileName: string; - getText(): string; + getText(range?: any): string; } interface QuickPickGist { block: Gist; diff --git a/src/utils/__tests__/prompt.test.ts b/src/utils/__tests__/prompt.test.ts new file mode 100644 index 0000000..99940c9 --- /dev/null +++ b/src/utils/__tests__/prompt.test.ts @@ -0,0 +1,24 @@ +// tslint:disable:no-any no-unsafe-any no-magic-numbers + +import { window } from 'vscode'; + +import { prompt } from '../prompt'; + +const showInputBoxSpy: jest.SpyInstance = jest.spyOn( + window, + 'showInputBox' +); + +describe('Prompt Tests', () => { + test('a prompt is made', async () => { + expect.assertions(2); + + await prompt('foo', 'bar'); + + expect(showInputBoxSpy.mock.calls).toHaveLength(1); + expect(showInputBoxSpy).toHaveBeenCalledWith({ + prompt: 'foo', + value: 'bar' + }); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index e9e7382..11905fd 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './file'; export * from './notify'; +export * from './prompt'; diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts new file mode 100644 index 0000000..34e7cbc --- /dev/null +++ b/src/utils/prompt.ts @@ -0,0 +1,16 @@ +import { window } from 'vscode'; + +export const prompt = async ( + message: string, + defaultValue?: string +): Promise => { + try { + const input = + (await window.showInputBox({ prompt: message, value: defaultValue })) || + ''; + + return input; + } catch (err) { + throw err; + } +};