Skip to content

Commit ea6d294

Browse files
committed
move to updated file for now
1 parent 8c3b367 commit ea6d294

File tree

4 files changed

+378
-1
lines changed

4 files changed

+378
-1
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
export const searchResponse = {
2+
query: 'document',
3+
locale: 'en-US',
4+
page: 1,
5+
pages: 383,
6+
start: 1,
7+
end: 10,
8+
next:
9+
'https://developer.mozilla.org/api/v1/search/en-US?highlight=false&page=2&q=document',
10+
previous: null,
11+
count: 3823,
12+
filters: [
13+
{
14+
name: 'Topics',
15+
slug: 'topic',
16+
options: [
17+
{
18+
name: 'APIs and DOM',
19+
slug: 'api',
20+
count: 2609,
21+
active: true,
22+
urls: {
23+
active: '/api/v1/search/en-US?highlight=false&q=document&topic=api',
24+
inactive: '/api/v1/search/en-US?highlight=false&q=document',
25+
},
26+
},
27+
],
28+
},
29+
],
30+
documents: [
31+
{
32+
title: 'Document directive',
33+
slug: 'Glossary/Document_directive',
34+
locale: 'en-US',
35+
excerpt:
36+
'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.',
37+
},
38+
{
39+
title: 'document environment',
40+
slug: 'Glossary/document_environment',
41+
locale: 'en-US',
42+
excerpt:
43+
"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.",
44+
},
45+
{
46+
title: 'DOM (Document Object Model)',
47+
slug: 'Glossary/DOM',
48+
locale: 'en-US',
49+
excerpt:
50+
'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).',
51+
},
52+
{
53+
title: 'Archived open Web documentation',
54+
slug: 'Archive/Web',
55+
locale: 'en-US',
56+
excerpt:
57+
'The documentation listed below is archived, obsolete material about open Web topics.',
58+
},
59+
{
60+
title: 'Document.documentElement',
61+
slug: 'Web/API/Document/documentElement',
62+
locale: 'en-US',
63+
excerpt:
64+
'Document.documentElement returns the Element that is the root element of the document (for example, the html element for HTML documents).',
65+
},
66+
{
67+
title: 'Document.documentURI',
68+
slug: 'Web/API/Document/documentURI',
69+
locale: 'en-US',
70+
excerpt:
71+
'The documentURI read-only property of the Document interface returns the document location as a string.',
72+
},
73+
{
74+
title: 'Document.documentURIObject',
75+
slug: 'Web/API/Document/documentURIObject',
76+
locale: 'en-US',
77+
excerpt:
78+
'The Document.documentURIObject read-only property returns an nsIURI object representing the URI of the document.',
79+
},
80+
{
81+
title: 'Document',
82+
slug: 'Web/API/Document',
83+
locale: 'en-US',
84+
excerpt:
85+
"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.",
86+
},
87+
{
88+
title: 'Document()',
89+
slug: 'Web/API/Document/Document',
90+
locale: 'en-US',
91+
excerpt:
92+
"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.",
93+
},
94+
{
95+
title: '@document',
96+
slug: 'Web/CSS/@document',
97+
locale: 'en-US',
98+
excerpt:
99+
'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.',
100+
},
101+
],
102+
};

src/commands/mdn/updated.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as DomParser from 'dom-parser';
2+
3+
import * as errors from '../../utils/errors';
4+
import { getSearchUrl } from '../../utils/urlTools';
5+
import useData from '../../utils/useData';
6+
7+
import { queryBuilder, updatedQueryBuilder } from '.';
8+
import { getChosenResult } from '../../utils/discordTools';
9+
import { searchResponse } from './__fixtures__/responses';
10+
11+
// jest.mock('dom-parser');
12+
// jest.mock('../../utils/urlTools');
13+
// jest.mock('../../utils/useData');
14+
15+
describe('handleMDNQuery', () => {
16+
const sendMock = jest.fn();
17+
const replyMock = jest.fn();
18+
const msg: any = {
19+
channel: { send: sendMock },
20+
reply: replyMock,
21+
};
22+
23+
const mockGetSearchUrl: jest.MockedFunction<typeof getSearchUrl> = getSearchUrl as any;
24+
const mockUseData: jest.MockedFunction<typeof useData> = useData as any;
25+
const mockChoose: jest.MockedFunction<typeof getChosenResult> = getChosenResult as any;
26+
27+
beforeEach(() => {
28+
mockGetSearchUrl.mockReturnValue('Search Term');
29+
});
30+
31+
afterEach(() => jest.resetAllMocks());
32+
33+
test('replies with invalid response error if search URL fails', async () => {
34+
mockUseData.mockResolvedValue({
35+
error: true,
36+
json: null,
37+
text: null,
38+
});
39+
40+
await queryBuilder()(msg, 'Search Term');
41+
42+
expect(msg.reply).toHaveBeenCalledWith(errors.invalidResponse);
43+
expect(msg.channel.send).not.toHaveBeenCalled();
44+
});
45+
46+
test('replies with 0 documents found', async () => {
47+
mockUseData.mockResolvedValue({
48+
error: false,
49+
json: null,
50+
text: 'Example',
51+
});
52+
53+
await queryBuilder(text => {
54+
expect(text).toEqual('Example');
55+
return {
56+
isEmpty: true,
57+
meta: '',
58+
results: [],
59+
};
60+
})(msg, 'Search Term');
61+
62+
expect(msg.reply).toBeCalledWith(errors.noResults('Search Term'));
63+
});
64+
65+
test('responds with list embedded', async () => {
66+
mockUseData.mockResolvedValue({
67+
error: false,
68+
json: null,
69+
text: 'Example',
70+
});
71+
72+
await queryBuilder(
73+
text => {
74+
expect(text).toEqual('Example');
75+
return {
76+
isEmpty: false,
77+
meta: '',
78+
results: [
79+
{
80+
getElementsByClassName(
81+
className: string
82+
): DomParser.Node[] | null {
83+
if (className === 'result-title') {
84+
return [
85+
{
86+
getAttribute() {
87+
return 'http://example.com';
88+
},
89+
textContent: '',
90+
} as any,
91+
];
92+
}
93+
return [
94+
{
95+
textContent: 'Some markdown',
96+
} as any,
97+
];
98+
},
99+
} as any,
100+
],
101+
};
102+
},
103+
() => ({
104+
excerpt: '',
105+
title: 'Example',
106+
url: 'http://www.example.com',
107+
}),
108+
() =>
109+
[
110+
{
111+
url: 'http://www.example.com',
112+
},
113+
] as any
114+
)(msg, 'Search Term');
115+
116+
expect(msg.channel.send).toHaveBeenCalledTimes(1);
117+
const sentMessage = msg.channel.send.mock.calls[0][0];
118+
expect(sentMessage).toMatchSnapshot();
119+
});
120+
});
121+
122+
describe('updatedMDNQuery', () => {
123+
const sendMock = jest.fn();
124+
const replyMock = jest.fn();
125+
const msg: any = {
126+
channel: { send: sendMock },
127+
reply: replyMock,
128+
};
129+
const editMock = {
130+
edit: jest.fn(),
131+
};
132+
133+
const mockUseData: jest.MockedFunction<typeof useData> = jest.fn();
134+
const mockChoose: jest.MockedFunction<typeof getChosenResult> = jest.fn();
135+
136+
test('should work', async () => {
137+
mockUseData.mockResolvedValueOnce({
138+
error: false,
139+
text: null,
140+
json: searchResponse,
141+
});
142+
sendMock.mockResolvedValue(editMock);
143+
mockChoose.mockResolvedValueOnce({
144+
title: 'DOM (Document Object Model)',
145+
slug: 'Glossary/DOM',
146+
locale: 'en-US',
147+
excerpt:
148+
'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).',
149+
});
150+
151+
const handler = updatedQueryBuilder(mockUseData, mockChoose);
152+
153+
await handler(msg, 'Document');
154+
expect(editMock.edit.mock.calls).toMatchSnapshot();
155+
});
156+
});

src/commands/mdn/updated.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* eslint-disable unicorn/prefer-query-selector */
2+
import { Message } from 'discord.js';
3+
4+
import delayedMessageAutoDeletion from '../../utils/delayedMessageAutoDeletion';
5+
import {
6+
adjustTitleLength,
7+
attemptEdit,
8+
BASE_DESCRIPTION,
9+
createDescription,
10+
createListEmbed,
11+
createMarkdownLink,
12+
createMarkdownListItem,
13+
getChosenResult,
14+
} from '../../utils/discordTools';
15+
import * as errors from '../../utils/errors';
16+
import { buildDirectUrl, getSearchUrl } from '../../utils/urlTools';
17+
import useData from '../../utils/useData';
18+
19+
const provider = 'mdn';
20+
21+
interface SearchResponse {
22+
query: string;
23+
locale: string;
24+
page: number;
25+
pages: number;
26+
starts: number;
27+
end: number;
28+
next: string;
29+
previous: string | null;
30+
count: number;
31+
filter: Array<{
32+
name: string;
33+
slug: string;
34+
options: Array<{
35+
name: string;
36+
slug: string;
37+
count: number;
38+
active: boolean;
39+
urls: {
40+
active: string;
41+
inactive: string;
42+
};
43+
}>;
44+
}>;
45+
documents: Array<{
46+
title: string;
47+
slug: string;
48+
locale: string;
49+
excerpt: string;
50+
}>;
51+
}
52+
53+
export const updatedQueryBuilder = (
54+
fetch: typeof useData = useData,
55+
waitForChosenResult: typeof getChosenResult = getChosenResult
56+
) => async (msg: Message, searchTerm: string) => {
57+
try {
58+
const url = getSearchUrl(provider, searchTerm);
59+
const { error, json } = await fetch<SearchResponse>(url, 'json');
60+
if (error) {
61+
return msg.reply(errors.invalidResponse);
62+
}
63+
64+
if (json.documents.length === 0) {
65+
const sentMsg = await msg.reply(errors.noResults(searchTerm));
66+
return delayedMessageAutoDeletion(sentMsg);
67+
}
68+
69+
let preparedDescription = json.documents.map(
70+
({ title, excerpt, slug }, index) =>
71+
createMarkdownListItem(
72+
index,
73+
createMarkdownLink(
74+
adjustTitleLength([`**${title}**`, excerpt].join(' - ')),
75+
buildDirectUrl(provider, slug)
76+
)
77+
)
78+
);
79+
80+
const expectedLength = preparedDescription.reduce(
81+
(sum, item) => sum + item.length,
82+
0
83+
);
84+
if (expectedLength + BASE_DESCRIPTION.length + 10 * '\n'.length > 2048) {
85+
preparedDescription = preparedDescription.map(string => {
86+
// split at markdown link ending
87+
const [title, ...rest] = string.split('...]');
88+
89+
// split title on title - excerpt glue
90+
// concat with rest
91+
// fix broken markdown link ending
92+
return [title.split(' - ')[0], rest.join('')].join(']');
93+
});
94+
}
95+
96+
const sentMsg = await msg.channel.send(
97+
createListEmbed({
98+
description: createDescription(preparedDescription),
99+
footerText: `${json.documents.length} results found`,
100+
provider,
101+
searchTerm,
102+
url,
103+
})
104+
);
105+
106+
const result = await waitForChosenResult(sentMsg, msg, json.documents);
107+
if (!result) {
108+
return;
109+
}
110+
111+
const editableUrl = buildDirectUrl(provider, `/` + result.slug);
112+
await attemptEdit(sentMsg, editableUrl, { embed: null });
113+
} catch (error) {
114+
console.error(error);
115+
await msg.reply(errors.unknownError);
116+
}
117+
};
118+
119+
export default updatedQueryBuilder();

src/utils/urlTools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const buildDirectUrl = (provider: Provider, href: string) => {
121121
return providers[provider].direct.replace(TERM, href);
122122
}
123123

124-
throw new Error(`provider not implemeted: ${provider}`);
124+
throw new Error(`provider not implemented: ${provider}`);
125125
};
126126

127127
export const getExtendedInfoUrl = (provider: Provider, term: string) => {

0 commit comments

Comments
 (0)