mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
12 commits on master since v4.7.1 baseline, taking tests 524 → 914 across 27 new suites. This commit: - Bumps package.json + README version to v4.8.0 (additive features → semver minor) - Applies Prettier formatting to all files touched during the overnight session so release build is reproducible from a clean repo New in v4.8.0: - PDF encryption close-out (D1) - KaTeX rendering test coverage - Autosave buffer + crash recovery banner - Daily notes + workspace search + doc-aware Q&A - Search sidebar panel + Ask mode - Status bar: word count, reading time, Flesch-Kincaid grade - Footnote hover preview - Smart paste (URL → link, CSV/TSV → table) - Daily notes menu item + sidebar panel + Q&A deep-link - Daily templates gallery - Pluggable DocQA engine (TF-idF default + lazy neural embeddings) Amit Haridas
203 lines
7.5 KiB
JavaScript
203 lines
7.5 KiB
JavaScript
/**
|
|
* @jest-environment node
|
|
*
|
|
* UrlTitle tests — pure module with injectable fetch.
|
|
*/
|
|
const UrlTitle = require('../../src/main/UrlTitle');
|
|
|
|
describe('UrlTitle.isHttpUrl', () => {
|
|
test('accepts http and https URLs', () => {
|
|
expect(UrlTitle.isHttpUrl('http://example.com')).toBe(true);
|
|
expect(UrlTitle.isHttpUrl('https://example.com/path?x=1')).toBe(true);
|
|
});
|
|
|
|
test('rejects non-http(s) schemes', () => {
|
|
expect(UrlTitle.isHttpUrl('ftp://example.com')).toBe(false);
|
|
expect(UrlTitle.isHttpUrl('javascript:alert(1)')).toBe(false);
|
|
expect(UrlTitle.isHttpUrl('data:text/plain,hello')).toBe(false);
|
|
expect(UrlTitle.isHttpUrl('file:///etc/passwd')).toBe(false);
|
|
});
|
|
|
|
test('rejects malformed / non-string input', () => {
|
|
expect(UrlTitle.isHttpUrl('not a url')).toBe(false);
|
|
expect(UrlTitle.isHttpUrl(null)).toBe(false);
|
|
expect(UrlTitle.isHttpUrl(undefined)).toBe(false);
|
|
expect(UrlTitle.isHttpUrl(42)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('UrlTitle.decodeTitle', () => {
|
|
test('decodes named entities', () => {
|
|
expect(UrlTitle.decodeTitle('AT&T <Home>')).toBe('AT&T <Home>');
|
|
});
|
|
|
|
test('decodes numeric and hex entities', () => {
|
|
expect(UrlTitle.decodeTitle('Café')).toBe('Café');
|
|
expect(UrlTitle.decodeTitle('—mdash')).toBe('—mdash');
|
|
});
|
|
|
|
test('does not decode entities introduced by the strip step (tags-as-text survive)', () => {
|
|
// Confirms that decoded entities like <b> → <b> stay visible as text
|
|
// rather than being stripped as a tag. (Stripping would break the named-
|
|
// entity decoding test above.)
|
|
expect(UrlTitle.decodeTitle('A <b>B</b> C')).toBe('A <b>B</b> C');
|
|
});
|
|
|
|
test('collapses whitespace and trims', () => {
|
|
expect(UrlTitle.decodeTitle(' hello\n\nworld ')).toBe('hello world');
|
|
});
|
|
|
|
test('returns empty string for non-string input', () => {
|
|
expect(UrlTitle.decodeTitle(null)).toBe('');
|
|
expect(UrlTitle.decodeTitle(undefined)).toBe('');
|
|
expect(UrlTitle.decodeTitle(42)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('UrlTitle.extractTitleFromHtml', () => {
|
|
test('extracts the first <title> in the body', () => {
|
|
expect(
|
|
UrlTitle.extractTitleFromHtml(
|
|
'<html><head><title>Hello World</title></head><body></body></html>'
|
|
)
|
|
).toBe('Hello World');
|
|
});
|
|
|
|
test('decodes entities in the title', () => {
|
|
expect(
|
|
UrlTitle.extractTitleFromHtml('<html><head><title>News & Updates</title></head></html>')
|
|
).toBe('News & Updates');
|
|
});
|
|
|
|
test('returns null when there is no <title>', () => {
|
|
expect(UrlTitle.extractTitleFromHtml('<html><head></head></html>')).toBeNull();
|
|
});
|
|
|
|
test('is case-insensitive on the tag', () => {
|
|
expect(UrlTitle.extractTitleFromHtml('<TITLE>Mixed Case</TITLE>')).toBe('Mixed Case');
|
|
});
|
|
|
|
test('returns null for non-string input', () => {
|
|
expect(UrlTitle.extractTitleFromHtml(null)).toBeNull();
|
|
expect(UrlTitle.extractTitleFromHtml(undefined)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('UrlTitle.fetchTitle — success path', () => {
|
|
test('returns {url, title} when the response is HTML with a <title>', async () => {
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: (k) => (k === 'content-type' ? 'text/html; charset=utf-8' : null) },
|
|
text: async () => '<html><head><title>Hello & Welcome</title></head></html>',
|
|
});
|
|
const result = await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch });
|
|
expect(result).toEqual({ url: 'https://x.com', title: 'Hello & Welcome' });
|
|
});
|
|
|
|
test('caps long titles at 200 chars with an ellipsis', async () => {
|
|
const long = 'A'.repeat(500);
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: () => 'text/html' },
|
|
text: async () => `<title>${long}</title>`,
|
|
});
|
|
const r = await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch });
|
|
expect(r.title.length).toBe(201); // 200 + ellipsis
|
|
expect(r.title.endsWith('…')).toBe(true);
|
|
});
|
|
|
|
test('handles a streaming response (ReadableStream body)', async () => {
|
|
const html = '<title>Streamed</title>';
|
|
const encoder = new TextEncoder();
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: () => 'text/html' },
|
|
body: {
|
|
getReader: () => ({
|
|
read: jest
|
|
.fn()
|
|
.mockResolvedValueOnce({ value: encoder.encode(html.slice(0, 13)), done: false })
|
|
.mockResolvedValueOnce({ value: encoder.encode(html.slice(13)), done: false })
|
|
.mockResolvedValueOnce({ value: undefined, done: true }),
|
|
cancel: jest.fn().mockResolvedValue(undefined),
|
|
}),
|
|
},
|
|
});
|
|
const r = await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch });
|
|
expect(r.title).toBe('Streamed');
|
|
});
|
|
});
|
|
|
|
describe('UrlTitle.fetchTitle — failure paths', () => {
|
|
test('returns null when the URL is not http(s)', async () => {
|
|
const fakeFetch = jest.fn();
|
|
expect(await UrlTitle.fetchTitle({ url: 'javascript:alert(1)', fetch: fakeFetch })).toBeNull();
|
|
expect(fakeFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('returns null when fetch is unavailable', async () => {
|
|
expect(await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: null })).toBeNull();
|
|
});
|
|
|
|
test('returns null when the response is non-OK', async () => {
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: false,
|
|
headers: { get: () => 'text/html' },
|
|
});
|
|
expect(await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch })).toBeNull();
|
|
});
|
|
|
|
test('returns null when the content-type is not HTML', async () => {
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: () => 'application/octet-stream' },
|
|
text: async () => '<title>not html</title>',
|
|
});
|
|
expect(await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch })).toBeNull();
|
|
});
|
|
|
|
test('returns null when there is no <title>', async () => {
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: () => 'text/html' },
|
|
text: async () => '<html><body>no title</body></html>',
|
|
});
|
|
expect(await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch })).toBeNull();
|
|
});
|
|
|
|
test('returns null when fetch rejects (network error)', async () => {
|
|
const fakeFetch = jest.fn().mockRejectedValue(new Error('econnreset'));
|
|
expect(await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch })).toBeNull();
|
|
});
|
|
|
|
test('returns null when the body exceeds maxBytes', async () => {
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: () => 'text/html' },
|
|
text: async () => '<title>t</title>' + 'x'.repeat(200),
|
|
});
|
|
expect(
|
|
await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch, maxBytes: 100 })
|
|
).toBeNull();
|
|
});
|
|
|
|
test('respects a streaming body that exceeds maxBytes (cancels the reader)', async () => {
|
|
const encoder = new TextEncoder();
|
|
const big = 'x'.repeat(1000);
|
|
const cancel = jest.fn().mockResolvedValue(undefined);
|
|
const fakeFetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
headers: { get: () => 'text/html' },
|
|
body: {
|
|
getReader: () => ({
|
|
read: jest.fn().mockResolvedValue({ value: encoder.encode(big), done: false }),
|
|
cancel,
|
|
}),
|
|
},
|
|
});
|
|
const r = await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch, maxBytes: 100 });
|
|
expect(r).toBeNull();
|
|
expect(cancel).toHaveBeenCalled();
|
|
});
|
|
});
|