merge(frontend): resolve conflicts with main — integrate fileHash feature into panel architecture
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 2m21s
CI / Backend Unit Tests (pull_request) Successful in 2m11s
CI / E2E Tests (pull_request) Failing after 28m37s
CI / Unit & Component Tests (push) Successful in 2m26s
CI / Backend Unit Tests (push) Successful in 2m14s
CI / E2E Tests (push) Has started running
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 2m21s
CI / Backend Unit Tests (pull_request) Successful in 2m11s
CI / E2E Tests (pull_request) Failing after 28m37s
CI / Unit & Component Tests (push) Successful in 2m26s
CI / Backend Unit Tests (push) Successful in 2m14s
CI / E2E Tests (push) Has started running
Keep the new bottom-panel / AnnotationSidePanel architecture from this branch while pulling in the documentFileHash / visibleAnnotations filter that was added on main. Thread documentFileHash through DocumentViewer so outdated-annotation filtering works end-to-end. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #63.
This commit is contained in:
@@ -216,3 +216,35 @@ test.describe('Admin — tag management', () => {
|
||||
await page.screenshot({ path: 'test-results/e2e/admin-tag-restored.png' });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── System tab — backfill file hashes ────────────────────────────────────────
|
||||
|
||||
test.describe('Admin system tab — backfill file hashes', () => {
|
||||
test('admin triggers file hash backfill and sees success message', async ({ request, page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
// Create a document via API so there is at least one without a hash
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Backfill Hash Test' }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create failed: ${createRes.status()}`);
|
||||
|
||||
await page.goto('/admin');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// Navigate to System tab
|
||||
await page.getByRole('button', { name: /system/i }).click();
|
||||
|
||||
// Click the backfill hashes button
|
||||
const btn = page.getByRole('button', { name: /datei-hashes berechnen/i });
|
||||
await expect(btn).toBeVisible();
|
||||
await btn.click();
|
||||
|
||||
// Success message must appear (count >= 0)
|
||||
await expect(page.locator('text=/\\d+ Dokumente wurden aktualisiert/i')).toBeVisible({
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
await page.screenshot({ path: 'test-results/e2e/admin-backfill-hashes.png' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,13 +347,142 @@ test.describe('PDF annotations — admin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── PDF Annotations — file hash (version awareness) ─────────────────────────
|
||||
|
||||
test.describe('PDF annotations — file hash versioning', () => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
const PDF_FIXTURE2 = path.resolve(__dirname, 'fixtures/minimal2.pdf');
|
||||
|
||||
test('annotations are hidden after a different file is uploaded', async ({ page, request }) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
// 1. Create document and upload original PDF
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Hash Test — version' }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create failed: ${createRes.status()}`);
|
||||
const doc = await createRes.json();
|
||||
|
||||
const uploadRes = await request.put(`/api/documents/${doc.id}`, {
|
||||
multipart: {
|
||||
title: doc.title,
|
||||
file: {
|
||||
name: 'minimal.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: fs.readFileSync(PDF_FIXTURE)
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!uploadRes.ok()) throw new Error(`Upload failed: ${uploadRes.status()}`);
|
||||
|
||||
// 2. Create an annotation via API
|
||||
const annotRes = await request.post(`/api/documents/${doc.id}/annotations`, {
|
||||
data: { pageNumber: 1, x: 0.1, y: 0.1, width: 0.2, height: 0.2, color: '#ff0000' }
|
||||
});
|
||||
if (!annotRes.ok()) throw new Error(`Create annotation failed: ${annotRes.status()}`);
|
||||
|
||||
// 3. Verify annotation appears before re-upload
|
||||
await page.goto(`${baseURL}/documents/${doc.id}`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
|
||||
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
|
||||
timeout: 8000
|
||||
});
|
||||
|
||||
// 4. Upload a different file (different hash)
|
||||
const reuploadRes = await request.put(`/api/documents/${doc.id}`, {
|
||||
multipart: {
|
||||
title: doc.title,
|
||||
file: {
|
||||
name: 'minimal2.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: fs.readFileSync(PDF_FIXTURE2)
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!reuploadRes.ok()) throw new Error(`Re-upload failed: ${reuploadRes.status()}`);
|
||||
|
||||
// 5. Reload — annotation must be hidden and notice shown
|
||||
await page.reload();
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
|
||||
|
||||
await expect(page.locator('[data-testid^="annotation-"]')).toHaveCount(0, { timeout: 8000 });
|
||||
await expect(page.locator('[data-testid="annotation-outdated-notice"]')).toBeVisible({
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
await page.screenshot({ path: 'test-results/e2e/annotation-hidden-after-reupload.png' });
|
||||
});
|
||||
|
||||
test('annotations reappear after re-uploading the original file', async ({ page, request }) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
// 1. Create document and upload original PDF
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Hash Test — restore' }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create failed: ${createRes.status()}`);
|
||||
const doc = await createRes.json();
|
||||
|
||||
const originalBytes = fs.readFileSync(PDF_FIXTURE);
|
||||
const uploadRes = await request.put(`/api/documents/${doc.id}`, {
|
||||
multipart: {
|
||||
title: doc.title,
|
||||
file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: originalBytes }
|
||||
}
|
||||
});
|
||||
if (!uploadRes.ok()) throw new Error(`Upload failed: ${uploadRes.status()}`);
|
||||
|
||||
// 2. Create annotation
|
||||
const annotRes = await request.post(`/api/documents/${doc.id}/annotations`, {
|
||||
data: { pageNumber: 1, x: 0.1, y: 0.1, width: 0.2, height: 0.2, color: '#0000ff' }
|
||||
});
|
||||
if (!annotRes.ok()) throw new Error(`Create annotation failed: ${annotRes.status()}`);
|
||||
|
||||
// 3. Replace with different file
|
||||
const replaceRes = await request.put(`/api/documents/${doc.id}`, {
|
||||
multipart: {
|
||||
title: doc.title,
|
||||
file: {
|
||||
name: 'minimal2.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: fs.readFileSync(PDF_FIXTURE2)
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!replaceRes.ok()) throw new Error(`Replace failed: ${replaceRes.status()}`);
|
||||
|
||||
// 4. Re-upload original file (restoring the hash)
|
||||
const restoreRes = await request.put(`/api/documents/${doc.id}`, {
|
||||
multipart: {
|
||||
title: doc.title,
|
||||
file: { name: 'minimal.pdf', mimeType: 'application/pdf', buffer: originalBytes }
|
||||
}
|
||||
});
|
||||
if (!restoreRes.ok()) throw new Error(`Restore failed: ${restoreRes.status()}`);
|
||||
|
||||
// 5. Verify annotation reappears and notice is gone
|
||||
await page.goto(`${baseURL}/documents/${doc.id}`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
|
||||
|
||||
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
|
||||
timeout: 8000
|
||||
});
|
||||
await expect(page.locator('[data-testid="annotation-outdated-notice"]')).not.toBeVisible();
|
||||
|
||||
await page.screenshot({ path: 'test-results/e2e/annotation-restored.png' });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── PDF Annotations (read-only user) ─────────────────────────────────────────
|
||||
|
||||
test.describe('PDF annotations — read-only user', () => {
|
||||
// Isolated session — does not share the admin storage state
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('read-only user sees a disabled Annotieren button', async ({ page }) => {
|
||||
test('read-only user does not see the Annotieren button', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Benutzername').fill('reader');
|
||||
@@ -365,12 +494,10 @@ test.describe('PDF annotations — read-only user', () => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
await page.goto(`${baseURL}/documents/${sharedAnnotationDocId}`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
// Wait for the PDF canvas — once rendered, the controls bar (with disabled button) is shown.
|
||||
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 30000 });
|
||||
|
||||
const disabledBtn = page.getByRole('button', { name: /annotieren/i });
|
||||
await expect(disabledBtn).toBeVisible({ timeout: 5000 });
|
||||
await expect(disabledBtn).toBeDisabled();
|
||||
// Reader users do not have ANNOTATE_ALL permission — the button must not be shown at all.
|
||||
const annotateBtn = page.getByRole('button', { name: /annotieren/i });
|
||||
await expect(annotateBtn).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
await page.screenshot({ path: 'test-results/e2e/annotations-button-reader.png' });
|
||||
});
|
||||
|
||||
21
frontend/e2e/fixtures/minimal2.pdf
Normal file
21
frontend/e2e/fixtures/minimal2.pdf
Normal file
@@ -0,0 +1,21 @@
|
||||
%PDF-1.4
|
||||
1 0 obj
|
||||
<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<</Type/Pages/Kids[3 0 R]/Count 1>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<</Type/Page/MediaBox[0 0 3 3]/Parent 2 0 R>>
|
||||
endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
trailer
|
||||
<</Size 4/Root 1 0 R>>
|
||||
startxref
|
||||
190
|
||||
%%EOF
|
||||
Reference in New Issue
Block a user