Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions e2e/storage/preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,38 @@ test.describe('Storage S3 — Preview', () => {
}
});

test('shows binary fallback preview for content types the server knows to be binary', async ({
page
}, testInfo) => {
const credentials = requireGarageCredentials();
const client = createS3Client(credentials);
const prefix = uniquePrefix(testInfo, 'preview-known-bin');
const cleanupKeys = [`${prefix}events.avro`];

try {
await client.send(
new PutObjectCommand({
Bucket: credentials.bucket,
Key: `${prefix}events.avro`,
// Avro object container magic bytes
Body: Buffer.from([0x4f, 0x62, 0x6a, 0x01, 0x00, 0x00]),
ContentType: 'application/avro'
})
);

await connectAndOpenPrefix(page, credentials, prefix);

await rowByName(page, 'events.avro').dblclick();

await expect(page.getByRole('heading', { name: 'events.avro' })).toBeVisible();
await expect(page.getByText('Binary content')).toBeVisible();
await expect(page.getByTestId('editor-readonly-notice')).toHaveCount(0);
await expect(page.getByRole('dialog').getByRole('application')).toHaveCount(0);
} finally {
await deleteKnownKeys(client, credentials.bucket, cleanupKeys);
}
});

test('shows a PDF preview for PDF files', async ({ page }, testInfo) => {
const credentials = requireGarageCredentials();
const client = createS3Client(credentials);
Expand Down
7 changes: 7 additions & 0 deletions src/lib/components/storage/modals/PreviewModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@
return;
}

// The server sends no body for content types it knows to be binary.
if (res.headers.get('X-Preview-Renderable') === 'false') {
await res.body?.cancel();
preview = { kind: 'fallback', contentType, isBinary: true };
return;
}

if (contentType.startsWith('image/')) {
if (truncated) {
await res.body?.cancel();
Expand Down
69 changes: 51 additions & 18 deletions src/lib/components/storage/modals/PreviewModal.svelte.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ function mockFetchResponse(
etag?: string;
}
): Response {
if (opts?.renderable === false) {
// Mirrors binaryPreview() in $lib/server/storage/preview/binary.ts.
return new Response(null, {
status: 200,
headers: {
'Content-Type': opts.contentType ?? 'application/octet-stream',
'X-Preview-Renderable': 'false',
'X-Preview-Total-Size': String(opts.totalSize ?? 11),
'X-Preview-Bytes': '0',
'X-Preview-Truncated': 'false',
'Cache-Control': 'no-store'
}
});
}
return new Response(body, {
status: 200,
headers: {
Expand Down Expand Up @@ -665,22 +679,35 @@ describe('PreviewModal basics', () => {
.not.toBeInTheDocument();
});

it('should render fallback when X-Preview-Renderable is false', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
mockFetchResponse('', {
contentType: 'application/zip',
renderable: false
})
)
);
render(PreviewModal, { ...defaultProps, objectKey: 'archive.zip' });

// Fallback should not show a footer with download/close buttons
// Wait for the preview to load
await expect.element(page.getByText('archive.zip')).toBeInTheDocument();
});
it.each([
['data/events.avro', 'application/avro'],
['logs/app.log.gz', 'application/gzip'],
['reports/q3.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
['archive.zip', 'application/zip']
])(
'should render the binary fallback when X-Preview-Renderable is false (%s)',
async (objectKey, contentType) => {
vi.stubGlobal(
'fetch',
vi
.fn()
.mockResolvedValue(
mockFetchResponse(null, { contentType, renderable: false, totalSize: 4096 })
)
);
render(PreviewModal, { ...defaultProps, objectKey });

await expect
.element(
page.getByText(
'This file appears to contain binary data and cannot be displayed as text.'
)
)
.toBeInTheDocument();
await expect.element(page.getByTestId('editor-readonly-notice')).not.toBeInTheDocument();
await expect.element(page.getByRole('application')).not.toBeInTheDocument();
}
);

it('should render fallback for binary content that fails UTF-8 decode', async () => {
// Create bytes that are invalid UTF-8 (not a CSV/TSV extension)
Expand All @@ -703,8 +730,14 @@ describe('PreviewModal basics', () => {
);
render(PreviewModal, { ...defaultProps, objectKey: 'data/file.bin' });

// Should eventually show fallback (no footer since fallback kind)
await expect.element(page.getByText('file.bin')).toBeInTheDocument();
await expect
.element(
page.getByText(
'This file appears to contain binary data and cannot be displayed as text.'
)
)
.toBeInTheDocument();
await expect.element(page.getByRole('application')).not.toBeInTheDocument();
});
});

Expand Down
Loading