Sender/receivers populated, filePath set, full user object, Escape vs other keys keydown handler, deep-link comment query. 6 new tests targeting ~14 branches. Refs #496. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
253 lines
7.1 KiB
TypeScript
253 lines
7.1 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
|
|
const mockPage = {
|
|
url: new URL('http://localhost/documents/d1'),
|
|
state: {}
|
|
};
|
|
|
|
vi.mock('$app/state', () => ({
|
|
get page() {
|
|
return mockPage;
|
|
}
|
|
}));
|
|
|
|
vi.mock('$app/navigation', () => ({
|
|
beforeNavigate: () => {},
|
|
afterNavigate: () => {},
|
|
goto: vi.fn(),
|
|
invalidate: vi.fn(),
|
|
invalidateAll: vi.fn(),
|
|
preloadCode: vi.fn(),
|
|
preloadData: vi.fn(),
|
|
pushState: vi.fn(),
|
|
replaceState: vi.fn(),
|
|
disableScrollHandling: vi.fn(),
|
|
onNavigate: () => () => {}
|
|
}));
|
|
|
|
vi.mock('$lib/shared/services/confirm.svelte', () => ({
|
|
getConfirmService: () => ({ confirm: async () => false })
|
|
}));
|
|
vi.mock('$lib/shared/services/confirm.svelte.js', () => ({
|
|
getConfirmService: () => ({ confirm: async () => false })
|
|
}));
|
|
|
|
const { default: DocumentDetailPage } = await import('./+page.svelte');
|
|
|
|
afterEach(cleanup);
|
|
|
|
const baseDoc = {
|
|
id: 'd1',
|
|
title: 'Brief an Helene',
|
|
originalFilename: 'brief.pdf',
|
|
documentDate: '1923-04-15',
|
|
sender: null,
|
|
receivers: [],
|
|
tags: [],
|
|
filePath: null,
|
|
contentType: null,
|
|
location: null,
|
|
status: 'UPLOADED',
|
|
fileHash: null
|
|
};
|
|
|
|
const baseData = (overrides: Record<string, unknown> = {}) => ({
|
|
document: baseDoc,
|
|
canWrite: false,
|
|
canBlogWrite: false,
|
|
user: null,
|
|
geschichten: [],
|
|
inferredRelationship: null,
|
|
...overrides
|
|
});
|
|
|
|
describe('documents/[id] page', () => {
|
|
it('renders the DocumentTopBar with the document title', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d1');
|
|
render(DocumentDetailPage, { props: { data: baseData() } });
|
|
|
|
// Just verify the page mounts and renders the top bar
|
|
const topbar = document.querySelector('[data-topbar]');
|
|
expect(topbar).not.toBeNull();
|
|
});
|
|
|
|
it('renders the DocumentViewer in the page', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d1');
|
|
render(DocumentDetailPage, { props: { data: baseData() } });
|
|
|
|
// DocumentViewer renders an absolute container; just check the page mounted
|
|
const main = document.body.firstElementChild;
|
|
expect(main).not.toBeNull();
|
|
});
|
|
|
|
it('persists last-visited document ID to localStorage on mount', async () => {
|
|
localStorage.removeItem('familienarchiv.lastVisited');
|
|
mockPage.url = new URL('http://localhost/documents/d1');
|
|
render(DocumentDetailPage, { props: { data: baseData() } });
|
|
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
const stored = localStorage.getItem('familienarchiv.lastVisited');
|
|
expect(stored).toContain('d1');
|
|
});
|
|
|
|
it('uses doc.title as the document title when set', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d1');
|
|
render(DocumentDetailPage, { props: { data: baseData() } });
|
|
|
|
// The browser <title> reflects doc.title when set
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
expect(document.title).toContain('Brief an Helene');
|
|
});
|
|
|
|
it('falls back to originalFilename when title is empty', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d2');
|
|
render(DocumentDetailPage, {
|
|
props: {
|
|
data: baseData({
|
|
document: { ...baseDoc, id: 'd2', title: '', originalFilename: 'fallback.pdf' }
|
|
})
|
|
}
|
|
});
|
|
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
expect(document.title).toContain('fallback.pdf');
|
|
});
|
|
|
|
it('falls back to "Dokument" when title and originalFilename are empty', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d3');
|
|
render(DocumentDetailPage, {
|
|
props: {
|
|
data: baseData({
|
|
document: { ...baseDoc, id: 'd3', title: '', originalFilename: '' }
|
|
})
|
|
}
|
|
});
|
|
|
|
await new Promise((r) => setTimeout(r, 30));
|
|
expect(document.title).toContain('Dokument');
|
|
});
|
|
|
|
it('renders without throwing when canWrite is true', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d4');
|
|
expect(() =>
|
|
render(DocumentDetailPage, { props: { data: baseData({ canWrite: true }) } })
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders without throwing when geschichten and inferredRelationship are set', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d5');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: {
|
|
data: baseData({
|
|
geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }],
|
|
inferredRelationship: { label: 'PARENT_OF', from: 'p1', to: 'p2' },
|
|
canBlogWrite: true
|
|
})
|
|
}
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders without throwing when doc.id is empty', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d-empty');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: { data: baseData({ document: { ...baseDoc, id: '', title: 'No ID' } }) }
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders with task=transcribe in the URL without throwing', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d6?task=transcribe');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: { data: baseData({ document: { ...baseDoc, id: 'd6' } }) }
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders without throwing with sender/receivers populated', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d7');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: {
|
|
data: baseData({
|
|
document: {
|
|
...baseDoc,
|
|
id: 'd7',
|
|
sender: { id: 's1', displayName: 'Anna Schmidt' },
|
|
receivers: [{ id: 'r1', displayName: 'Bert Meier' }]
|
|
}
|
|
})
|
|
}
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders without throwing when filePath is set on the document', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d8');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: {
|
|
data: baseData({
|
|
document: {
|
|
...baseDoc,
|
|
id: 'd8',
|
|
filePath: 's3://bucket/file.pdf',
|
|
contentType: 'application/pdf'
|
|
}
|
|
})
|
|
}
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders without throwing with a complete user object', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d9');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: {
|
|
data: baseData({
|
|
document: { ...baseDoc, id: 'd9' },
|
|
user: { id: 'u1', firstName: 'Anna', lastName: 'S', email: 'a@x' }
|
|
})
|
|
}
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('handles Escape keydown without throwing (close transcribe path)', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d10');
|
|
render(DocumentDetailPage, {
|
|
props: { data: baseData({ document: { ...baseDoc, id: 'd10' } }) }
|
|
});
|
|
|
|
expect(() =>
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('handles non-Escape keydown without firing close handler', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d11');
|
|
render(DocumentDetailPage, {
|
|
props: { data: baseData({ document: { ...baseDoc, id: 'd11' } }) }
|
|
});
|
|
|
|
expect(() =>
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('renders without throwing with a deep-link comment query param', async () => {
|
|
mockPage.url = new URL('http://localhost/documents/d12?comment=c-abc');
|
|
expect(() =>
|
|
render(DocumentDetailPage, {
|
|
props: { data: baseData({ document: { ...baseDoc, id: 'd12' } }) }
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
});
|