From 662d04611293d628530168cc74e16b2f36191b0f Mon Sep 17 00:00:00 2001 From: Nathan Yam Date: Thu, 11 Jun 2020 22:20:39 +1000 Subject: [PATCH 1/6] started on mdn json api usage --- src/commands/mdn/index.test.ts | 30 ++++++++++- src/commands/mdn/index.ts | 91 ++++++++++++++++++++++++++++++++++ src/utils/urlTools.ts | 2 +- 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src/commands/mdn/index.test.ts b/src/commands/mdn/index.test.ts index a2de48f7..6f052038 100644 --- a/src/commands/mdn/index.test.ts +++ b/src/commands/mdn/index.test.ts @@ -4,7 +4,9 @@ import * as errors from '../../utils/errors'; import { getSearchUrl } from '../../utils/urlTools'; import useData from '../../utils/useData'; -import { queryBuilder } from '.'; +import { queryBuilder, updatedQueryBuilder } from '.'; +import { getChosenResult } from '../../utils/discordTools'; +import { searchResponse } from './__fixtures__/responses'; jest.mock('dom-parser'); jest.mock('../../utils/urlTools'); @@ -12,6 +14,7 @@ jest.mock('../../utils/useData'); const mockGetSearchUrl: jest.MockedFunction = getSearchUrl as any; const mockUseData: jest.MockedFunction = useData as any; +const mockChoose: jest.MockedFunction = getChosenResult as any; describe('handleMDNQuery', () => { const sendMock = jest.fn(); @@ -116,3 +119,28 @@ describe('handleMDNQuery', () => { expect(sentMessage).toMatchSnapshot(); }); }); + +describe('updatedMDNQuery', () => { + const sendMock = jest.fn(); + const replyMock = jest.fn(); + const msg: any = { + channel: { send: sendMock }, + reply: replyMock, + }; + + test('should work', async () => { + mockGetSearchUrl.mockReturnValue('http://example.com'); + mockUseData.mockResolvedValueOnce({ + error: false, + text: null, + json: searchResponse, + }); + + const handler = updatedQueryBuilder( + mockGetSearchUrl, + mockUseData, + mockChoose + ); + await handler(msg, 'Search Term'); + }); +}); diff --git a/src/commands/mdn/index.ts b/src/commands/mdn/index.ts index e6e1a8c5..3ba8b105 100644 --- a/src/commands/mdn/index.ts +++ b/src/commands/mdn/index.ts @@ -27,6 +27,38 @@ interface ParserResult { meta: string; } +interface SearchResponse { + query: string; + locale: string; + page: number; + pages: number; + starts: number; + end: number; + next: string; + previous: string | null; + count: number; + filter: Array<{ + name: string; + slug: string; + options: Array<{ + name: string; + slug: string; + count: number; + active: boolean; + urls: { + active: string; + inactive: string; + }; + }>; + }>; + documents: Array<{ + title: string; + slug: string; + locale: string; + excerpt: string; + }>; +} + interface ResultMeta { getElementsByClassName(cls: string): DOMParser.Node[]; } @@ -75,6 +107,65 @@ const extractMetadataFromResult = (result: ResultMeta) => { }; }; +export const updatedQueryBuilder = ( + searchUrl: typeof getSearchUrl = getSearchUrl, + fetch: typeof useData = useData, + waitForChosenResult: typeof getChosenResult = getChosenResult +) => async (msg: Message, searchTerm: string) => { + const url = searchUrl(provider, searchTerm); + const { error, json } = await fetch(url, 'json'); + if (!error) { + return msg.reply(errors.invalidResponse); + } + + if (json.documents.length === 0) { + const sentMsg = await msg.reply(errors.noResults(searchTerm)); + return delayedMessageAutoDeletion(sentMsg); + } + + let preparedDescription = json.documents.map( + ({ title, excerpt, slug }, index) => + createMarkdownListItem( + index, + createMarkdownLink( + adjustTitleLength([`**${title}**`, excerpt].join(' - ')), + buildDirectUrl(provider, slug) + ) + ) + ); + + const expectedLength = preparedDescription.reduce( + (sum, item) => sum + item.length, + 0 + ); + if (expectedLength + BASE_DESCRIPTION.length + 10 * '\n'.length > 2048) { + preparedDescription = preparedDescription.map(string => { + // split at markdown link ending + const [title, ...rest] = string.split('...]'); + + // split title on title - excerpt glue + // concat with rest + // fix broken markdown link ending + return [title.split(' - ')[0], rest.join('')].join(']'); + }); + } + + const sentMsg = await msg.channel.send( + createListEmbed({ + description: createDescription(preparedDescription), + footerText: 'Powered by the search API', + provider, + searchTerm, + url, + }) + ); + + const result = await waitForChosenResult(sentMsg, msg, json.documents); + if (!result) { + return; + } +}; + /** * Poor man's dependency injection without introducing classes, just use closures * and higher order functions instead. Also provides a default so we don't have diff --git a/src/utils/urlTools.ts b/src/utils/urlTools.ts index c2d94611..5ffd7bb5 100644 --- a/src/utils/urlTools.ts +++ b/src/utils/urlTools.ts @@ -75,7 +75,7 @@ export const providers: ProviderMap = { createTitle: (searchTerm: string) => `NPM results for *${searchTerm}*`, help: '!npm react', icon: 'https://avatars0.githubusercontent.com/u/6078720', - search: `https://www.npmjs.com/search/suggestions?q=${SEARCH_TERM}`, + search: `https://developer.mozilla.org/api/v1/search/en-US?highlight=false&q=${SEARCH_TERM}`, }, php: { color: 0x8892bf, From a6a6ca7bef50a07a7f0a187910ab67e47af01b7c Mon Sep 17 00:00:00 2001 From: Nathan Yam Date: Thu, 11 Jun 2020 23:04:47 +1000 Subject: [PATCH 2/6] move to updated file for now --- src/commands/mdn/__fixtures__/responses.ts | 102 ++++++++++++++ src/commands/mdn/updated.test.ts | 156 +++++++++++++++++++++ src/commands/mdn/updated.ts | 119 ++++++++++++++++ src/utils/urlTools.ts | 2 +- 4 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 src/commands/mdn/__fixtures__/responses.ts create mode 100644 src/commands/mdn/updated.test.ts create mode 100644 src/commands/mdn/updated.ts diff --git a/src/commands/mdn/__fixtures__/responses.ts b/src/commands/mdn/__fixtures__/responses.ts new file mode 100644 index 00000000..fb6b5ced --- /dev/null +++ b/src/commands/mdn/__fixtures__/responses.ts @@ -0,0 +1,102 @@ +export const searchResponse = { + query: 'document', + locale: 'en-US', + page: 1, + pages: 383, + start: 1, + end: 10, + next: + 'https://developer.mozilla.org/api/v1/search/en-US?highlight=false&page=2&q=document', + previous: null, + count: 3823, + filters: [ + { + name: 'Topics', + slug: 'topic', + options: [ + { + name: 'APIs and DOM', + slug: 'api', + count: 2609, + active: true, + urls: { + active: '/api/v1/search/en-US?highlight=false&q=document&topic=api', + inactive: '/api/v1/search/en-US?highlight=false&q=document', + }, + }, + ], + }, + ], + documents: [ + { + title: 'Document directive', + slug: 'Glossary/Document_directive', + locale: 'en-US', + excerpt: + 'CSP document directives are used in a Content-Security-Policy header and govern the properties of a document or worker environment to which a policy applies.', + }, + { + title: 'document environment', + slug: 'Glossary/document_environment', + locale: 'en-US', + excerpt: + "When the JavaScript global environment is a window or an iframe, it is called a document environment. A global environment is an environment that doesn't have an outer environment.", + }, + { + title: 'DOM (Document Object Model)', + slug: 'Glossary/DOM', + locale: 'en-US', + excerpt: + 'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).', + }, + { + title: 'Archived open Web documentation', + slug: 'Archive/Web', + locale: 'en-US', + excerpt: + 'The documentation listed below is archived, obsolete material about open Web topics.', + }, + { + title: 'Document.documentElement', + slug: 'Web/API/Document/documentElement', + locale: 'en-US', + excerpt: + 'Document.documentElement returns the Element that is the root element of the document (for example, the html element for HTML documents).', + }, + { + title: 'Document.documentURI', + slug: 'Web/API/Document/documentURI', + locale: 'en-US', + excerpt: + 'The documentURI read-only property of the Document interface returns the document location as a string.', + }, + { + title: 'Document.documentURIObject', + slug: 'Web/API/Document/documentURIObject', + locale: 'en-US', + excerpt: + 'The Document.documentURIObject read-only property returns an nsIURI object representing the URI of the document.', + }, + { + title: 'Document', + slug: 'Web/API/Document', + locale: 'en-US', + excerpt: + "The Document interface represents any web page loaded in the browser and serves as an entry point into the web page's content, which is the DOM tree.", + }, + { + title: 'Document()', + slug: 'Web/API/Document/Document', + locale: 'en-US', + excerpt: + "The Document constructor creates a new Document object that is a web page loaded in the browser and serving as an entry point into the page's content.", + }, + { + title: '@document', + slug: 'Web/CSS/@document', + locale: 'en-US', + excerpt: + 'The @document CSS at-rule restricts the style rules contained within it based on the URL of the document. It is designed primarily for user-defined style sheets, though it can be used on author-defined style sheets, too.', + }, + ], +}; diff --git a/src/commands/mdn/updated.test.ts b/src/commands/mdn/updated.test.ts new file mode 100644 index 00000000..420d6e41 --- /dev/null +++ b/src/commands/mdn/updated.test.ts @@ -0,0 +1,156 @@ +import * as DomParser from 'dom-parser'; + +import * as errors from '../../utils/errors'; +import { getSearchUrl } from '../../utils/urlTools'; +import useData from '../../utils/useData'; + +import { queryBuilder, updatedQueryBuilder } from '.'; +import { getChosenResult } from '../../utils/discordTools'; +import { searchResponse } from './__fixtures__/responses'; + +// jest.mock('dom-parser'); +// jest.mock('../../utils/urlTools'); +// jest.mock('../../utils/useData'); + +describe('handleMDNQuery', () => { + const sendMock = jest.fn(); + const replyMock = jest.fn(); + const msg: any = { + channel: { send: sendMock }, + reply: replyMock, + }; + + const mockGetSearchUrl: jest.MockedFunction = getSearchUrl as any; + const mockUseData: jest.MockedFunction = useData as any; + const mockChoose: jest.MockedFunction = getChosenResult as any; + + beforeEach(() => { + mockGetSearchUrl.mockReturnValue('Search Term'); + }); + + afterEach(() => jest.resetAllMocks()); + + test('replies with invalid response error if search URL fails', async () => { + mockUseData.mockResolvedValue({ + error: true, + json: null, + text: null, + }); + + await queryBuilder()(msg, 'Search Term'); + + expect(msg.reply).toHaveBeenCalledWith(errors.invalidResponse); + expect(msg.channel.send).not.toHaveBeenCalled(); + }); + + test('replies with 0 documents found', async () => { + mockUseData.mockResolvedValue({ + error: false, + json: null, + text: 'Example', + }); + + await queryBuilder(text => { + expect(text).toEqual('Example'); + return { + isEmpty: true, + meta: '', + results: [], + }; + })(msg, 'Search Term'); + + expect(msg.reply).toBeCalledWith(errors.noResults('Search Term')); + }); + + test('responds with list embedded', async () => { + mockUseData.mockResolvedValue({ + error: false, + json: null, + text: 'Example', + }); + + await queryBuilder( + text => { + expect(text).toEqual('Example'); + return { + isEmpty: false, + meta: '', + results: [ + { + getElementsByClassName( + className: string + ): DomParser.Node[] | null { + if (className === 'result-title') { + return [ + { + getAttribute() { + return 'http://example.com'; + }, + textContent: '', + } as any, + ]; + } + return [ + { + textContent: 'Some markdown', + } as any, + ]; + }, + } as any, + ], + }; + }, + () => ({ + excerpt: '', + title: 'Example', + url: 'http://www.example.com', + }), + () => + [ + { + url: 'http://www.example.com', + }, + ] as any + )(msg, 'Search Term'); + + expect(msg.channel.send).toHaveBeenCalledTimes(1); + const sentMessage = msg.channel.send.mock.calls[0][0]; + expect(sentMessage).toMatchSnapshot(); + }); +}); + +describe('updatedMDNQuery', () => { + const sendMock = jest.fn(); + const replyMock = jest.fn(); + const msg: any = { + channel: { send: sendMock }, + reply: replyMock, + }; + const editMock = { + edit: jest.fn(), + }; + + const mockUseData: jest.MockedFunction = jest.fn(); + const mockChoose: jest.MockedFunction = jest.fn(); + + test('should work', async () => { + mockUseData.mockResolvedValueOnce({ + error: false, + text: null, + json: searchResponse, + }); + sendMock.mockResolvedValue(editMock); + mockChoose.mockResolvedValueOnce({ + title: 'DOM (Document Object Model)', + slug: 'Glossary/DOM', + locale: 'en-US', + excerpt: + 'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).', + }); + + const handler = updatedQueryBuilder(mockUseData, mockChoose); + + await handler(msg, 'Document'); + expect(editMock.edit.mock.calls).toMatchSnapshot(); + }); +}); diff --git a/src/commands/mdn/updated.ts b/src/commands/mdn/updated.ts new file mode 100644 index 00000000..3b375ea7 --- /dev/null +++ b/src/commands/mdn/updated.ts @@ -0,0 +1,119 @@ +/* eslint-disable unicorn/prefer-query-selector */ +import { Message } from 'discord.js'; + +import delayedMessageAutoDeletion from '../../utils/delayedMessageAutoDeletion'; +import { + adjustTitleLength, + attemptEdit, + BASE_DESCRIPTION, + createDescription, + createListEmbed, + createMarkdownLink, + createMarkdownListItem, + getChosenResult, +} from '../../utils/discordTools'; +import * as errors from '../../utils/errors'; +import { buildDirectUrl, getSearchUrl } from '../../utils/urlTools'; +import useData from '../../utils/useData'; + +const provider = 'mdn'; + +interface SearchResponse { + query: string; + locale: string; + page: number; + pages: number; + starts: number; + end: number; + next: string; + previous: string | null; + count: number; + filter: Array<{ + name: string; + slug: string; + options: Array<{ + name: string; + slug: string; + count: number; + active: boolean; + urls: { + active: string; + inactive: string; + }; + }>; + }>; + documents: Array<{ + title: string; + slug: string; + locale: string; + excerpt: string; + }>; +} + +export const updatedQueryBuilder = ( + fetch: typeof useData = useData, + waitForChosenResult: typeof getChosenResult = getChosenResult +) => async (msg: Message, searchTerm: string) => { + try { + const url = getSearchUrl(provider, searchTerm); + const { error, json } = await fetch(url, 'json'); + if (error) { + return msg.reply(errors.invalidResponse); + } + + if (json.documents.length === 0) { + const sentMsg = await msg.reply(errors.noResults(searchTerm)); + return delayedMessageAutoDeletion(sentMsg); + } + + let preparedDescription = json.documents.map( + ({ title, excerpt, slug }, index) => + createMarkdownListItem( + index, + createMarkdownLink( + adjustTitleLength([`**${title}**`, excerpt].join(' - ')), + buildDirectUrl(provider, slug) + ) + ) + ); + + const expectedLength = preparedDescription.reduce( + (sum, item) => sum + item.length, + 0 + ); + if (expectedLength + BASE_DESCRIPTION.length + 10 * '\n'.length > 2048) { + preparedDescription = preparedDescription.map(string => { + // split at markdown link ending + const [title, ...rest] = string.split('...]'); + + // split title on title - excerpt glue + // concat with rest + // fix broken markdown link ending + return [title.split(' - ')[0], rest.join('')].join(']'); + }); + } + + const sentMsg = await msg.channel.send( + createListEmbed({ + description: createDescription(preparedDescription), + footerText: `${json.documents.length} results found`, + provider, + searchTerm, + url, + }) + ); + + const result = await waitForChosenResult(sentMsg, msg, json.documents); + if (!result) { + return; + } + + const editableUrl = buildDirectUrl(provider, `/` + result.slug); + await attemptEdit(sentMsg, editableUrl, { embed: null }); + } catch (error) { + console.error(error); + await msg.reply(errors.unknownError); + } +}; + +export default updatedQueryBuilder(); diff --git a/src/utils/urlTools.ts b/src/utils/urlTools.ts index 5ffd7bb5..77e9089f 100644 --- a/src/utils/urlTools.ts +++ b/src/utils/urlTools.ts @@ -123,7 +123,7 @@ export const buildDirectUrl = (provider: Provider, href: string) => { return providers[provider].direct.replace(TERM, href); } - throw new Error(`provider not implemeted: ${provider}`); + throw new Error(`provider not implemented: ${provider}`); }; export const getExtendedInfoUrl = (provider: Provider, term: string) => { From e5f4ba53b7ccff5bfeeb671fbe5d6b0946587e21 Mon Sep 17 00:00:00 2001 From: Nathan Yam Date: Thu, 11 Jun 2020 23:20:32 +1000 Subject: [PATCH 3/6] Rename mdn strategies --- .../{index.test.ts.snap => dom.test.ts.snap} | 0 src/commands/mdn/api.test.ts | 43 +++++ src/commands/mdn/{updated.ts => api.ts} | 0 .../mdn/{index.test.ts => dom.test.ts} | 28 +--- src/commands/mdn/{index.ts => dom.ts} | 59 ------- src/commands/mdn/updated.test.ts | 156 ------------------ src/index.ts | 2 +- 7 files changed, 45 insertions(+), 243 deletions(-) rename src/commands/mdn/__snapshots__/{index.test.ts.snap => dom.test.ts.snap} (100%) create mode 100644 src/commands/mdn/api.test.ts rename src/commands/mdn/{updated.ts => api.ts} (100%) rename src/commands/mdn/{index.test.ts => dom.test.ts} (82%) rename src/commands/mdn/{index.ts => dom.ts} (75%) delete mode 100644 src/commands/mdn/updated.test.ts diff --git a/src/commands/mdn/__snapshots__/index.test.ts.snap b/src/commands/mdn/__snapshots__/dom.test.ts.snap similarity index 100% rename from src/commands/mdn/__snapshots__/index.test.ts.snap rename to src/commands/mdn/__snapshots__/dom.test.ts.snap diff --git a/src/commands/mdn/api.test.ts b/src/commands/mdn/api.test.ts new file mode 100644 index 00000000..a628c546 --- /dev/null +++ b/src/commands/mdn/api.test.ts @@ -0,0 +1,43 @@ +import { updatedQueryBuilder } from './api'; + +import { searchResponse } from './__fixtures__/responses'; +import useData from '../../utils/useData'; +import { getChosenResult } from '../../utils/discordTools'; + +describe('updatedMDNQuery', () => { + const mockUseData: jest.MockedFunction = jest.fn(); + const mockChoose: jest.MockedFunction = jest.fn(); + + const editMsg = { + edit: jest.fn(), + }; + const sendMock = jest.fn(); + const replyMock = jest.fn(); + const msg: any = { + channel: { send: sendMock }, + reply: replyMock, + }; + + test('should work', async () => { + mockUseData.mockResolvedValueOnce({ + error: false, + text: null, + json: searchResponse, + }); + + mockChoose.mockResolvedValueOnce({ + title: 'DOM (Document Object Model)', + slug: 'Glossary/DOM', + locale: 'en-US', + excerpt: + 'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).', + }); + + sendMock.mockResolvedValue(editMsg); + const handler = updatedQueryBuilder(mockUseData, mockChoose); + + await handler(msg, 'Search Term'); + expect(msg.channel.send.mock.calls).toMatchSnapshot(); + expect(editMsg.edit.mock.calls).toMatchSnapshot(); + }); +}); diff --git a/src/commands/mdn/updated.ts b/src/commands/mdn/api.ts similarity index 100% rename from src/commands/mdn/updated.ts rename to src/commands/mdn/api.ts diff --git a/src/commands/mdn/index.test.ts b/src/commands/mdn/dom.test.ts similarity index 82% rename from src/commands/mdn/index.test.ts rename to src/commands/mdn/dom.test.ts index 6f052038..eb5f867a 100644 --- a/src/commands/mdn/index.test.ts +++ b/src/commands/mdn/dom.test.ts @@ -4,9 +4,8 @@ import * as errors from '../../utils/errors'; import { getSearchUrl } from '../../utils/urlTools'; import useData from '../../utils/useData'; -import { queryBuilder, updatedQueryBuilder } from '.'; +import { queryBuilder } from './dom'; import { getChosenResult } from '../../utils/discordTools'; -import { searchResponse } from './__fixtures__/responses'; jest.mock('dom-parser'); jest.mock('../../utils/urlTools'); @@ -119,28 +118,3 @@ describe('handleMDNQuery', () => { expect(sentMessage).toMatchSnapshot(); }); }); - -describe('updatedMDNQuery', () => { - const sendMock = jest.fn(); - const replyMock = jest.fn(); - const msg: any = { - channel: { send: sendMock }, - reply: replyMock, - }; - - test('should work', async () => { - mockGetSearchUrl.mockReturnValue('http://example.com'); - mockUseData.mockResolvedValueOnce({ - error: false, - text: null, - json: searchResponse, - }); - - const handler = updatedQueryBuilder( - mockGetSearchUrl, - mockUseData, - mockChoose - ); - await handler(msg, 'Search Term'); - }); -}); diff --git a/src/commands/mdn/index.ts b/src/commands/mdn/dom.ts similarity index 75% rename from src/commands/mdn/index.ts rename to src/commands/mdn/dom.ts index 3ba8b105..475bcccc 100644 --- a/src/commands/mdn/index.ts +++ b/src/commands/mdn/dom.ts @@ -107,65 +107,6 @@ const extractMetadataFromResult = (result: ResultMeta) => { }; }; -export const updatedQueryBuilder = ( - searchUrl: typeof getSearchUrl = getSearchUrl, - fetch: typeof useData = useData, - waitForChosenResult: typeof getChosenResult = getChosenResult -) => async (msg: Message, searchTerm: string) => { - const url = searchUrl(provider, searchTerm); - const { error, json } = await fetch(url, 'json'); - if (!error) { - return msg.reply(errors.invalidResponse); - } - - if (json.documents.length === 0) { - const sentMsg = await msg.reply(errors.noResults(searchTerm)); - return delayedMessageAutoDeletion(sentMsg); - } - - let preparedDescription = json.documents.map( - ({ title, excerpt, slug }, index) => - createMarkdownListItem( - index, - createMarkdownLink( - adjustTitleLength([`**${title}**`, excerpt].join(' - ')), - buildDirectUrl(provider, slug) - ) - ) - ); - - const expectedLength = preparedDescription.reduce( - (sum, item) => sum + item.length, - 0 - ); - if (expectedLength + BASE_DESCRIPTION.length + 10 * '\n'.length > 2048) { - preparedDescription = preparedDescription.map(string => { - // split at markdown link ending - const [title, ...rest] = string.split('...]'); - - // split title on title - excerpt glue - // concat with rest - // fix broken markdown link ending - return [title.split(' - ')[0], rest.join('')].join(']'); - }); - } - - const sentMsg = await msg.channel.send( - createListEmbed({ - description: createDescription(preparedDescription), - footerText: 'Powered by the search API', - provider, - searchTerm, - url, - }) - ); - - const result = await waitForChosenResult(sentMsg, msg, json.documents); - if (!result) { - return; - } -}; - /** * Poor man's dependency injection without introducing classes, just use closures * and higher order functions instead. Also provides a default so we don't have diff --git a/src/commands/mdn/updated.test.ts b/src/commands/mdn/updated.test.ts deleted file mode 100644 index 420d6e41..00000000 --- a/src/commands/mdn/updated.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import * as DomParser from 'dom-parser'; - -import * as errors from '../../utils/errors'; -import { getSearchUrl } from '../../utils/urlTools'; -import useData from '../../utils/useData'; - -import { queryBuilder, updatedQueryBuilder } from '.'; -import { getChosenResult } from '../../utils/discordTools'; -import { searchResponse } from './__fixtures__/responses'; - -// jest.mock('dom-parser'); -// jest.mock('../../utils/urlTools'); -// jest.mock('../../utils/useData'); - -describe('handleMDNQuery', () => { - const sendMock = jest.fn(); - const replyMock = jest.fn(); - const msg: any = { - channel: { send: sendMock }, - reply: replyMock, - }; - - const mockGetSearchUrl: jest.MockedFunction = getSearchUrl as any; - const mockUseData: jest.MockedFunction = useData as any; - const mockChoose: jest.MockedFunction = getChosenResult as any; - - beforeEach(() => { - mockGetSearchUrl.mockReturnValue('Search Term'); - }); - - afterEach(() => jest.resetAllMocks()); - - test('replies with invalid response error if search URL fails', async () => { - mockUseData.mockResolvedValue({ - error: true, - json: null, - text: null, - }); - - await queryBuilder()(msg, 'Search Term'); - - expect(msg.reply).toHaveBeenCalledWith(errors.invalidResponse); - expect(msg.channel.send).not.toHaveBeenCalled(); - }); - - test('replies with 0 documents found', async () => { - mockUseData.mockResolvedValue({ - error: false, - json: null, - text: 'Example', - }); - - await queryBuilder(text => { - expect(text).toEqual('Example'); - return { - isEmpty: true, - meta: '', - results: [], - }; - })(msg, 'Search Term'); - - expect(msg.reply).toBeCalledWith(errors.noResults('Search Term')); - }); - - test('responds with list embedded', async () => { - mockUseData.mockResolvedValue({ - error: false, - json: null, - text: 'Example', - }); - - await queryBuilder( - text => { - expect(text).toEqual('Example'); - return { - isEmpty: false, - meta: '', - results: [ - { - getElementsByClassName( - className: string - ): DomParser.Node[] | null { - if (className === 'result-title') { - return [ - { - getAttribute() { - return 'http://example.com'; - }, - textContent: '', - } as any, - ]; - } - return [ - { - textContent: 'Some markdown', - } as any, - ]; - }, - } as any, - ], - }; - }, - () => ({ - excerpt: '', - title: 'Example', - url: 'http://www.example.com', - }), - () => - [ - { - url: 'http://www.example.com', - }, - ] as any - )(msg, 'Search Term'); - - expect(msg.channel.send).toHaveBeenCalledTimes(1); - const sentMessage = msg.channel.send.mock.calls[0][0]; - expect(sentMessage).toMatchSnapshot(); - }); -}); - -describe('updatedMDNQuery', () => { - const sendMock = jest.fn(); - const replyMock = jest.fn(); - const msg: any = { - channel: { send: sendMock }, - reply: replyMock, - }; - const editMock = { - edit: jest.fn(), - }; - - const mockUseData: jest.MockedFunction = jest.fn(); - const mockChoose: jest.MockedFunction = jest.fn(); - - test('should work', async () => { - mockUseData.mockResolvedValueOnce({ - error: false, - text: null, - json: searchResponse, - }); - sendMock.mockResolvedValue(editMock); - mockChoose.mockResolvedValueOnce({ - title: 'DOM (Document Object Model)', - slug: 'Glossary/DOM', - locale: 'en-US', - excerpt: - 'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).', - }); - - const handler = updatedQueryBuilder(mockUseData, mockChoose); - - await handler(msg, 'Document'); - expect(editMock.edit.mock.calls).toMatchSnapshot(); - }); -}); diff --git a/src/index.ts b/src/index.ts index a269445e..1b4998ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import handleFormattingRequest from './commands/formatting'; import handleGithubQuery from './commands/github'; import handleJQueryCommand from './commands/jquery'; import handleLeaderboardRequest from './commands/leaderboard'; -import handleMDNQuery from './commands/mdn'; +import handleMDNQuery from './commands/mdn/dom'; import handleNPMQuery from './commands/npm'; import handlePHPQuery from './commands/php'; import handlePointsRequest from './commands/points'; From 6ef1c067177d4806502d396aa083dc2cf1e5a46d Mon Sep 17 00:00:00 2001 From: Nathan Yam Date: Fri, 12 Jun 2020 23:12:05 +1000 Subject: [PATCH 4/6] Working out index issue --- src/commands/mdn/__fixtures__/responses.ts | 102 ----------------- .../mdn/__snapshots__/api.test.ts.snap | 47 ++++++++ src/commands/mdn/api.test.ts | 104 +++++++++++++++++- src/commands/mdn/api.ts | 2 +- src/commands/mdn/index.test.ts | 0 src/utils/urlTools.ts | 2 +- 6 files changed, 152 insertions(+), 105 deletions(-) delete mode 100644 src/commands/mdn/__fixtures__/responses.ts create mode 100644 src/commands/mdn/__snapshots__/api.test.ts.snap create mode 100644 src/commands/mdn/index.test.ts diff --git a/src/commands/mdn/__fixtures__/responses.ts b/src/commands/mdn/__fixtures__/responses.ts deleted file mode 100644 index fb6b5ced..00000000 --- a/src/commands/mdn/__fixtures__/responses.ts +++ /dev/null @@ -1,102 +0,0 @@ -export const searchResponse = { - query: 'document', - locale: 'en-US', - page: 1, - pages: 383, - start: 1, - end: 10, - next: - 'https://developer.mozilla.org/api/v1/search/en-US?highlight=false&page=2&q=document', - previous: null, - count: 3823, - filters: [ - { - name: 'Topics', - slug: 'topic', - options: [ - { - name: 'APIs and DOM', - slug: 'api', - count: 2609, - active: true, - urls: { - active: '/api/v1/search/en-US?highlight=false&q=document&topic=api', - inactive: '/api/v1/search/en-US?highlight=false&q=document', - }, - }, - ], - }, - ], - documents: [ - { - title: 'Document directive', - slug: 'Glossary/Document_directive', - locale: 'en-US', - excerpt: - 'CSP document directives are used in a Content-Security-Policy header and govern the properties of a document or worker environment to which a policy applies.', - }, - { - title: 'document environment', - slug: 'Glossary/document_environment', - locale: 'en-US', - excerpt: - "When the JavaScript global environment is a window or an iframe, it is called a document environment. A global environment is an environment that doesn't have an outer environment.", - }, - { - title: 'DOM (Document Object Model)', - slug: 'Glossary/DOM', - locale: 'en-US', - excerpt: - 'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).', - }, - { - title: 'Archived open Web documentation', - slug: 'Archive/Web', - locale: 'en-US', - excerpt: - 'The documentation listed below is archived, obsolete material about open Web topics.', - }, - { - title: 'Document.documentElement', - slug: 'Web/API/Document/documentElement', - locale: 'en-US', - excerpt: - 'Document.documentElement returns the Element that is the root element of the document (for example, the html element for HTML documents).', - }, - { - title: 'Document.documentURI', - slug: 'Web/API/Document/documentURI', - locale: 'en-US', - excerpt: - 'The documentURI read-only property of the Document interface returns the document location as a string.', - }, - { - title: 'Document.documentURIObject', - slug: 'Web/API/Document/documentURIObject', - locale: 'en-US', - excerpt: - 'The Document.documentURIObject read-only property returns an nsIURI object representing the URI of the document.', - }, - { - title: 'Document', - slug: 'Web/API/Document', - locale: 'en-US', - excerpt: - "The Document interface represents any web page loaded in the browser and serves as an entry point into the web page's content, which is the DOM tree.", - }, - { - title: 'Document()', - slug: 'Web/API/Document/Document', - locale: 'en-US', - excerpt: - "The Document constructor creates a new Document object that is a web page loaded in the browser and serving as an entry point into the page's content.", - }, - { - title: '@document', - slug: 'Web/CSS/@document', - locale: 'en-US', - excerpt: - 'The @document CSS at-rule restricts the style rules contained within it based on the URL of the document. It is designed primarily for user-defined style sheets, though it can be used on author-defined style sheets, too.', - }, - ], -}; diff --git a/src/commands/mdn/__snapshots__/api.test.ts.snap b/src/commands/mdn/__snapshots__/api.test.ts.snap new file mode 100644 index 00000000..8ac12a63 --- /dev/null +++ b/src/commands/mdn/__snapshots__/api.test.ts.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`updatedMDNQuery should work 1`] = ` +Array [ + Array [ + Object { + "embed": Object { + "author": null, + "color": 8638706, + "description": "1. [**Document directive** - CSP document directives are used in a Conten...](https://developer.mozilla.orgGlossary/Document_directive) +2. [**document environment** - When the JavaScript global environment is ...](https://developer.mozilla.orgGlossary/document_environment) +3. [**DOM (Document Object Model)** - The DOM (Document Object Model) is ...](https://developer.mozilla.orgGlossary/DOM) +4. [**Archived open Web documentation** - The documentation listed below ...](https://developer.mozilla.orgArchive/Web) +5. [**Document.documentElement** - Document.documentElement returns the E...](https://developer.mozilla.orgWeb/API/Document/documentElement) +6. [**Document.documentURI** - The documentURI read-only property of the ...](https://developer.mozilla.orgWeb/API/Document/documentURI) +7. [**Document.documentURIObject** - The Document.documentURIObject read-...](https://developer.mozilla.orgWeb/API/Document/documentURIObject) +8. [**Document** - The Document interface represents any web page loaded ...](https://developer.mozilla.orgWeb/API/Document) +9. [**Document()** - The Document constructor creates a new Document obje...](https://developer.mozilla.orgWeb/API/Document/Document) +10. [**@document** - The @document CSS at-rule restricts the style rules c...](https://developer.mozilla.orgWeb/CSS/@document) + +:bulb: *react with a number (:one:, :two:, ...) to filter your result* +:neutral_face: *react with \`❌\` to delete* +:point_up: *supports \`!mdn\`, \`!github\`, \`!caniuse\`, \`!npm\`, \`!composer\`, \`!bundlephobia\`, and \`!php\`* +:gear: *issues? feature requests? head over to [github](https://github.com/ljosberinn/webdev-support-bot)*", + "fields": Array [], + "footer": Object { + "iconURL": "https://avatars0.githubusercontent.com/u/7565578", + "text": "10 results found", + }, + "title": "MDN results for *Search Term*", + "url": "https://developer.mozilla.org/en-US/search?q=Search%20Term", + }, + }, + ], +] +`; + +exports[`updatedMDNQuery should work 2`] = ` +Array [ + Array [ + "https://developer.mozilla.org/Glossary/DOM", + Object { + "embed": null, + }, + ], +] +`; diff --git a/src/commands/mdn/api.test.ts b/src/commands/mdn/api.test.ts index a628c546..f1714b89 100644 --- a/src/commands/mdn/api.test.ts +++ b/src/commands/mdn/api.test.ts @@ -1,9 +1,111 @@ import { updatedQueryBuilder } from './api'; -import { searchResponse } from './__fixtures__/responses'; import useData from '../../utils/useData'; import { getChosenResult } from '../../utils/discordTools'; +const searchResponse = { + query: 'document', + locale: 'en-US', + page: 1, + pages: 383, + start: 1, + end: 10, + next: + 'https://developer.mozilla.org/api/v1/search/en-US?highlight=false&page=2&q=document', + previous: null, + count: 3823, + filters: [ + { + name: 'Topics', + slug: 'topic', + options: [ + { + name: 'APIs and DOM', + slug: 'api', + count: 2609, + active: true, + urls: { + active: '/api/v1/search/en-US?highlight=false&q=document&topic=api', + inactive: '/api/v1/search/en-US?highlight=false&q=document', + }, + }, + ], + }, + ], + documents: [ + { + title: 'Document directive', + slug: 'Glossary/Document_directive', + locale: 'en-US', + excerpt: + 'CSP document directives are used in a Content-Security-Policy header and govern the properties of a document or worker environment to which a policy applies.', + }, + { + title: 'document environment', + slug: 'Glossary/document_environment', + locale: 'en-US', + excerpt: + "When the JavaScript global environment is a window or an iframe, it is called a document environment. A global environment is an environment that doesn't have an outer environment.", + }, + { + title: 'DOM (Document Object Model)', + slug: 'Glossary/DOM', + locale: 'en-US', + excerpt: + 'The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).', + }, + { + title: 'Archived open Web documentation', + slug: 'Archive/Web', + locale: 'en-US', + excerpt: + 'The documentation listed below is archived, obsolete material about open Web topics.', + }, + { + title: 'Document.documentElement', + slug: 'Web/API/Document/documentElement', + locale: 'en-US', + excerpt: + 'Document.documentElement returns the Element that is the root element of the document (for example, the html element for HTML documents).', + }, + { + title: 'Document.documentURI', + slug: 'Web/API/Document/documentURI', + locale: 'en-US', + excerpt: + 'The documentURI read-only property of the Document interface returns the document location as a string.', + }, + { + title: 'Document.documentURIObject', + slug: 'Web/API/Document/documentURIObject', + locale: 'en-US', + excerpt: + 'The Document.documentURIObject read-only property returns an nsIURI object representing the URI of the document.', + }, + { + title: 'Document', + slug: 'Web/API/Document', + locale: 'en-US', + excerpt: + "The Document interface represents any web page loaded in the browser and serves as an entry point into the web page's content, which is the DOM tree.", + }, + { + title: 'Document()', + slug: 'Web/API/Document/Document', + locale: 'en-US', + excerpt: + "The Document constructor creates a new Document object that is a web page loaded in the browser and serving as an entry point into the page's content.", + }, + { + title: '@document', + slug: 'Web/CSS/@document', + locale: 'en-US', + excerpt: + 'The @document CSS at-rule restricts the style rules contained within it based on the URL of the document. It is designed primarily for user-defined style sheets, though it can be used on author-defined style sheets, too.', + }, + ], +}; + describe('updatedMDNQuery', () => { const mockUseData: jest.MockedFunction = jest.fn(); const mockChoose: jest.MockedFunction = jest.fn(); diff --git a/src/commands/mdn/api.ts b/src/commands/mdn/api.ts index 3b375ea7..6667222d 100644 --- a/src/commands/mdn/api.ts +++ b/src/commands/mdn/api.ts @@ -108,7 +108,7 @@ export const updatedQueryBuilder = ( return; } - const editableUrl = buildDirectUrl(provider, `/` + result.slug); + const editableUrl = buildDirectUrl(provider, result.slug); await attemptEdit(sentMsg, editableUrl, { embed: null }); } catch (error) { console.error(error); diff --git a/src/commands/mdn/index.test.ts b/src/commands/mdn/index.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/utils/urlTools.ts b/src/utils/urlTools.ts index 77e9089f..4643387d 100644 --- a/src/utils/urlTools.ts +++ b/src/utils/urlTools.ts @@ -65,7 +65,7 @@ export const providers: ProviderMap = { mdn: { color: 0x83d0f2, createTitle: (searchTerm: string) => `MDN results for *${searchTerm}*`, - direct: `https://developer.mozilla.org${TERM}`, + direct: `https://developer.mozilla.org/${TERM}`, help: '!mdn localStorage', icon: 'https://avatars0.githubusercontent.com/u/7565578', search: `https://developer.mozilla.org/en-US/search?q=${SEARCH_TERM}`, From e5c44152e8291e7ccacecd74e3330999a02fa7a6 Mon Sep 17 00:00:00 2001 From: Nathan Yam Date: Fri, 12 Jun 2020 23:16:19 +1000 Subject: [PATCH 5/6] Update snapshot --- .../mdn/__snapshots__/api.test.ts.snap | 20 +++++++++---------- src/commands/mdn/index.test.ts | 0 2 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 src/commands/mdn/index.test.ts diff --git a/src/commands/mdn/__snapshots__/api.test.ts.snap b/src/commands/mdn/__snapshots__/api.test.ts.snap index 8ac12a63..d00177da 100644 --- a/src/commands/mdn/__snapshots__/api.test.ts.snap +++ b/src/commands/mdn/__snapshots__/api.test.ts.snap @@ -7,16 +7,16 @@ Array [ "embed": Object { "author": null, "color": 8638706, - "description": "1. [**Document directive** - CSP document directives are used in a Conten...](https://developer.mozilla.orgGlossary/Document_directive) -2. [**document environment** - When the JavaScript global environment is ...](https://developer.mozilla.orgGlossary/document_environment) -3. [**DOM (Document Object Model)** - The DOM (Document Object Model) is ...](https://developer.mozilla.orgGlossary/DOM) -4. [**Archived open Web documentation** - The documentation listed below ...](https://developer.mozilla.orgArchive/Web) -5. [**Document.documentElement** - Document.documentElement returns the E...](https://developer.mozilla.orgWeb/API/Document/documentElement) -6. [**Document.documentURI** - The documentURI read-only property of the ...](https://developer.mozilla.orgWeb/API/Document/documentURI) -7. [**Document.documentURIObject** - The Document.documentURIObject read-...](https://developer.mozilla.orgWeb/API/Document/documentURIObject) -8. [**Document** - The Document interface represents any web page loaded ...](https://developer.mozilla.orgWeb/API/Document) -9. [**Document()** - The Document constructor creates a new Document obje...](https://developer.mozilla.orgWeb/API/Document/Document) -10. [**@document** - The @document CSS at-rule restricts the style rules c...](https://developer.mozilla.orgWeb/CSS/@document) + "description": "1. [**Document directive** - CSP document directives are used in a Conten...](https://developer.mozilla.org/Glossary/Document_directive) +2. [**document environment** - When the JavaScript global environment is ...](https://developer.mozilla.org/Glossary/document_environment) +3. [**DOM (Document Object Model)** - The DOM (Document Object Model) is ...](https://developer.mozilla.org/Glossary/DOM) +4. [**Archived open Web documentation** - The documentation listed below ...](https://developer.mozilla.org/Archive/Web) +5. [**Document.documentElement** - Document.documentElement returns the E...](https://developer.mozilla.org/Web/API/Document/documentElement) +6. [**Document.documentURI** - The documentURI read-only property of the ...](https://developer.mozilla.org/Web/API/Document/documentURI) +7. [**Document.documentURIObject** - The Document.documentURIObject read-...](https://developer.mozilla.org/Web/API/Document/documentURIObject) +8. [**Document** - The Document interface represents any web page loaded ...](https://developer.mozilla.org/Web/API/Document) +9. [**Document()** - The Document constructor creates a new Document obje...](https://developer.mozilla.org/Web/API/Document/Document) +10. [**@document** - The @document CSS at-rule restricts the style rules c...](https://developer.mozilla.org/Web/CSS/@document) :bulb: *react with a number (:one:, :two:, ...) to filter your result* :neutral_face: *react with \`❌\` to delete* diff --git a/src/commands/mdn/index.test.ts b/src/commands/mdn/index.test.ts deleted file mode 100644 index e69de29b..00000000 From 42d1ac9d3c07f74f80a608e2ff8d089b0a7c9908 Mon Sep 17 00:00:00 2001 From: Nathan Yam Date: Fri, 12 Jun 2020 23:19:12 +1000 Subject: [PATCH 6/6] Prefer API import --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 1b4998ea..0cfb693e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import handleFormattingRequest from './commands/formatting'; import handleGithubQuery from './commands/github'; import handleJQueryCommand from './commands/jquery'; import handleLeaderboardRequest from './commands/leaderboard'; -import handleMDNQuery from './commands/mdn/dom'; +import handleMDNQuery from './commands/mdn/api'; import handleNPMQuery from './commands/npm'; import handlePHPQuery from './commands/php'; import handlePointsRequest from './commands/points';