chore: update TypeScript build info cache

This commit is contained in:
2025-09-28 23:04:52 -04:00
parent a8b60f87f6
commit 1c7a49fd0e
40 changed files with 4337 additions and 126 deletions
+127
View File
@@ -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;
}
});
+29
View File
@@ -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);
}
}