chore: update TypeScript build info cache
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert/strict';
|
||||
import { DownloadService } from './download.service.js';
|
||||
|
||||
const makeFakeDocument = () => {
|
||||
const appendCalls: unknown[] = [];
|
||||
const removeCalls: unknown[] = [];
|
||||
const anchor = {
|
||||
href: '',
|
||||
download: '',
|
||||
rel: '',
|
||||
style: {} as Record<string, unknown>,
|
||||
clickCount: 0,
|
||||
click() {
|
||||
this.clickCount += 1;
|
||||
},
|
||||
};
|
||||
|
||||
const documentMock = {
|
||||
createElement(tag: string) {
|
||||
if (tag !== 'a') {
|
||||
throw new Error(`Unexpected element request: ${tag}`);
|
||||
}
|
||||
return anchor;
|
||||
},
|
||||
body: {
|
||||
appendChild(node: unknown) {
|
||||
appendCalls.push(node);
|
||||
},
|
||||
removeChild(node: unknown) {
|
||||
removeCalls.push(node);
|
||||
},
|
||||
},
|
||||
} as unknown as Document;
|
||||
|
||||
return { documentMock, anchor, appendCalls, removeCalls };
|
||||
};
|
||||
|
||||
test('DownloadService downloads normalized UTF-8 content', async () => {
|
||||
const service = new DownloadService();
|
||||
const originalDocument = globalThis.document;
|
||||
const NativeURL = globalThis.URL;
|
||||
|
||||
const { documentMock, anchor, appendCalls, removeCalls } = makeFakeDocument();
|
||||
let lastBlob: Blob | null = null;
|
||||
const revoked: string[] = [];
|
||||
|
||||
const urlMock = Object.assign(
|
||||
function MockURL(url: string | URL, base?: string | URL) {
|
||||
return new NativeURL(url as string, base as string);
|
||||
},
|
||||
{
|
||||
prototype: NativeURL.prototype,
|
||||
canParse: () => true,
|
||||
parse: (url: string | URL, base?: string | URL) => new NativeURL(url as string, base as string),
|
||||
createObjectURL(blob: Blob) {
|
||||
lastBlob = blob;
|
||||
return 'blob:test-url';
|
||||
},
|
||||
revokeObjectURL(url: string) {
|
||||
revoked.push(url);
|
||||
},
|
||||
}
|
||||
) as unknown as typeof URL;
|
||||
|
||||
globalThis.document = documentMock;
|
||||
globalThis.URL = urlMock;
|
||||
|
||||
try {
|
||||
service.downloadText('line1\r\nline2', 'note.md');
|
||||
|
||||
assert.equal(anchor.download, 'note.md');
|
||||
assert.equal(anchor.rel, 'noopener');
|
||||
assert.equal(anchor.clickCount, 1);
|
||||
assert.equal(appendCalls.length, 1);
|
||||
assert.equal(removeCalls.length, 1);
|
||||
assert.equal(revoked.length, 1);
|
||||
assert.ok(lastBlob, 'Blob should be created');
|
||||
const blobContent = await lastBlob!.text();
|
||||
assert.equal(blobContent, 'line1\nline2');
|
||||
} finally {
|
||||
globalThis.document = originalDocument;
|
||||
globalThis.URL = NativeURL;
|
||||
}
|
||||
});
|
||||
|
||||
test('DownloadService warns when content is empty', () => {
|
||||
const service = new DownloadService();
|
||||
const originalDocument = globalThis.document;
|
||||
const NativeURL = globalThis.URL;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
const { documentMock } = makeFakeDocument();
|
||||
const urlMock = Object.assign(
|
||||
function MockURL(url: string | URL, base?: string | URL) {
|
||||
return new NativeURL(url as string, base as string);
|
||||
},
|
||||
{
|
||||
prototype: NativeURL.prototype,
|
||||
canParse: () => true,
|
||||
parse: (url: string | URL, base?: string | URL) => new NativeURL(url as string, base as string),
|
||||
createObjectURL() {
|
||||
return 'blob:noop';
|
||||
},
|
||||
revokeObjectURL() {
|
||||
// noop
|
||||
},
|
||||
}
|
||||
) as unknown as typeof URL;
|
||||
|
||||
globalThis.document = documentMock;
|
||||
globalThis.URL = urlMock;
|
||||
|
||||
let warned = false;
|
||||
console.warn = () => {
|
||||
warned = true;
|
||||
};
|
||||
|
||||
try {
|
||||
service.downloadText('', 'empty.md');
|
||||
assert.equal(warned, true);
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
globalThis.document = originalDocument;
|
||||
globalThis.URL = NativeURL;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DownloadService {
|
||||
downloadText(content: string, filename: string, mime = 'text/markdown;charset=utf-8'): void {
|
||||
if (!content) {
|
||||
console.warn('[DownloadService] Empty content provided for download.');
|
||||
}
|
||||
|
||||
const normalized = content.replace(/\r\n/g, '\n');
|
||||
const blob = new Blob([normalized], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.rel = 'noopener';
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} finally {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert/strict';
|
||||
import { firstValueFrom, skip, take } from 'rxjs';
|
||||
import { MarkdownViewerService } from './markdown-viewer.service.js';
|
||||
import type { Note } from '../../types.js';
|
||||
|
||||
test('MarkdownViewerService exposes current note streams', async () => {
|
||||
const service = new MarkdownViewerService();
|
||||
|
||||
const note: Note = {
|
||||
id: 'note-1',
|
||||
title: 'Test note',
|
||||
content: 'rendered content',
|
||||
rawContent: 'raw markdown',
|
||||
tags: [],
|
||||
frontmatter: {},
|
||||
backlinks: [],
|
||||
mtime: Date.now(),
|
||||
fileName: 'note-1.md',
|
||||
filePath: '/note-1.md',
|
||||
originalPath: 'note-1',
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
};
|
||||
|
||||
service.setCurrentNote(note);
|
||||
|
||||
const observedNote = await firstValueFrom(service.getCurrentNote$().pipe(skip(1), take(1)));
|
||||
const observedRaw = await firstValueFrom(service.getCurrentRaw$().pipe(skip(1), take(1)));
|
||||
const observedFilename = await firstValueFrom(service.getCurrentFilename$().pipe(skip(1), take(1)));
|
||||
|
||||
assert.equal(observedNote?.id, note.id);
|
||||
assert.equal(observedRaw, note.rawContent);
|
||||
assert.equal(observedFilename, note.fileName);
|
||||
});
|
||||
|
||||
test('MarkdownViewerService falls back to content when rawContent missing', async () => {
|
||||
const service = new MarkdownViewerService();
|
||||
|
||||
const note: Note = {
|
||||
id: 'note-2',
|
||||
title: 'Another note',
|
||||
content: 'fallback content',
|
||||
rawContent: undefined as unknown as string,
|
||||
tags: [],
|
||||
frontmatter: {},
|
||||
backlinks: [],
|
||||
mtime: Date.now(),
|
||||
fileName: 'note-2.md',
|
||||
filePath: '/note-2.md',
|
||||
originalPath: 'note-2',
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
};
|
||||
|
||||
service.setCurrentNote(note);
|
||||
|
||||
const observedRaw = await firstValueFrom(service.getCurrentRaw$().pipe(skip(1), take(1)));
|
||||
assert.equal(observedRaw, note.content);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { Note } from '../../types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MarkdownViewerService {
|
||||
private readonly currentNoteSubject = new BehaviorSubject<Note | null>(null);
|
||||
private readonly currentRawSubject = new BehaviorSubject<string>('');
|
||||
private readonly currentFilenameSubject = new BehaviorSubject<string | null>(null);
|
||||
|
||||
getCurrentNote$(): Observable<Note | null> {
|
||||
return this.currentNoteSubject.asObservable();
|
||||
}
|
||||
|
||||
getCurrentRaw$(): Observable<string> {
|
||||
return this.currentRawSubject.asObservable();
|
||||
}
|
||||
|
||||
getCurrentFilename$(): Observable<string | null> {
|
||||
return this.currentFilenameSubject.asObservable();
|
||||
}
|
||||
|
||||
setCurrentNote(note: Note | null): void {
|
||||
if (!note) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentNoteSubject.next(note);
|
||||
const raw = typeof note.rawContent === 'string' ? note.rawContent : note.content;
|
||||
this.currentRawSubject.next(raw ?? '');
|
||||
this.currentFilenameSubject.next(note.fileName ?? null);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.currentNoteSubject.next(null);
|
||||
this.currentRawSubject.next('');
|
||||
this.currentFilenameSubject.next(null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user