feat(e2e): add file preview tests with route mocking for S3

Use Playwright page.route() to mock the file list API, getObject API,
and S3 download URLs. This allows testing text, markdown, and code
preview rendering without a real S3 backend. Verifies:
- Mobile: preview opens as full-screen drawer with content rendered
- Desktop: preview opens as centered dialog with content rendered
- File header shows name and size correctly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saltbo
2026-04-12 20:34:51 -04:00
parent 130adfe886
commit e3b257e2ba
4 changed files with 124 additions and 44 deletions
+1
View File
@@ -0,0 +1 @@
{"hello": "world"}
+3
View File
@@ -0,0 +1,3 @@
# Sample Markdown
This is a **test** file.
+1
View File
@@ -0,0 +1 @@
Hello, this is a plain text file for E2E testing.
+119 -44
View File
@@ -1,40 +1,86 @@
import { expect, test } from '@playwright/test'
import { createFolder, signUpAndGoToFiles } from './helpers'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { expect, type Page, test } from '@playwright/test'
import { signUpAndGoToFiles } from './helpers'
const FAKE_S3 = 'https://fake-s3.e2e.local'
const fixturesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures')
function fakeFile(name: string, type: string, size: number) {
const id = `mock-${name.replace(/\W/g, '-')}`
return {
id,
orgId: 'org',
alias: '',
name,
type,
size,
dirtype: 0, // FILE
parent: '',
object: `mock/${name}`,
storageId: 'storage',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
}
/** Set up route mocks so clicking a file triggers preview with fixture data. */
async function setupPreviewMocks(page: Page, file: { name: string; type: string; size: number; fixture: string }) {
const mock = fakeFile(file.name, file.type, file.size)
const downloadUrl = `${FAKE_S3}/${file.name}`
// Mock file list to include our fake file
await page.route('**/api/objects?*', async (route) => {
const resp = await route.fetch()
const body = await resp.json()
body.items = [mock, ...(body.items ?? [])]
body.total = body.items.length
return route.fulfill({ json: body })
})
// Mock getObject → return file with downloadUrl
await page.route(`**/api/objects/${mock.id}`, (route) => {
if (route.request().method() !== 'GET') return route.continue()
return route.fulfill({ json: { ...mock, downloadUrl } })
})
// Mock S3 download → serve fixture
await page.route(`${FAKE_S3}/**`, (route) =>
route.fulfill({ path: path.join(fixturesDir, file.fixture), contentType: file.type }),
)
return mock
}
// ---------------------------------------------------------------------------
// Preview dialog: mobile uses full-screen drawer, desktop uses centered dialog
// Preview per file type: mobile drawer vs desktop dialog
// ---------------------------------------------------------------------------
test.describe('Preview responsive layout', () => {
test('mobile: preview opens as full-screen drawer (not centered dialog)', async ({ page }, testInfo) => {
test.describe('Preview with mocked files', () => {
test('mobile: text file renders in full-screen drawer', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile', 'mobile only')
await signUpAndGoToFiles(page)
await createFolder(page, 'test-preview')
await setupPreviewMocks(page, { name: 'readme.txt', type: 'text/plain', size: 50, fixture: 'sample.txt' })
// We need a file to trigger preview — check if any files exist
const fileRows = page.locator('table tbody tr')
const count = await fileRows.count()
if (count === 0) {
test.skip(true, 'no files available to preview')
}
// Reload to get mocked file list
await page.reload()
await expect(page.getByText('readme.txt')).toBeVisible({ timeout: 10000 })
// Click the first file's row actions to trigger preview
const firstFileRow = fileRows.first()
await firstFileRow.locator('button').last().click()
// Click file name to open preview
await page.getByRole('button', { name: 'readme.txt' }).click()
// Look for preview/open menu item
const previewItem = page.getByRole('menuitem', { name: /preview|open/i })
if (!(await previewItem.isVisible({ timeout: 2000 }).catch(() => false))) {
test.skip(true, 'no previewable file found')
}
await previewItem.click()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10000 })
// On mobile, the preview should use a drawer (Sheet) that covers full viewport
const previewDialog = page.getByRole('dialog')
await expect(previewDialog).toBeVisible({ timeout: 5000 })
// Header shows file name
await expect(dialog.locator('p', { hasText: 'readme.txt' })).toBeVisible()
// The drawer should be near-full-screen on mobile
const bounds = await previewDialog.boundingBox()
// Content is rendered
await expect(dialog.getByText('Hello, this is a plain text file')).toBeVisible({ timeout: 10000 })
// Drawer is full-screen on mobile
const bounds = await dialog.boundingBox()
if (bounds) {
const viewport = page.viewportSize()!
expect(bounds.height).toBeGreaterThan(viewport.height * 0.9)
@@ -42,36 +88,65 @@ test.describe('Preview responsive layout', () => {
}
})
test('desktop: preview opens as centered dialog', async ({ page }, testInfo) => {
test('mobile: markdown file renders in drawer', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile', 'mobile only')
await signUpAndGoToFiles(page)
await setupPreviewMocks(page, { name: 'docs.md', type: 'text/markdown', size: 44, fixture: 'sample.md' })
await page.reload()
await expect(page.getByText('docs.md')).toBeVisible({ timeout: 10000 })
await page.getByRole('button', { name: 'docs.md' }).click()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10000 })
await expect(dialog.locator('p', { hasText: 'docs.md' })).toBeVisible()
})
test('mobile: code file renders in drawer', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile', 'mobile only')
await signUpAndGoToFiles(page)
await setupPreviewMocks(page, { name: 'config.json', type: 'application/json', size: 19, fixture: 'sample.json' })
await page.reload()
await expect(page.getByText('config.json')).toBeVisible({ timeout: 10000 })
await page.getByRole('button', { name: 'config.json' }).click()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10000 })
await expect(dialog.locator('p', { hasText: 'config.json' })).toBeVisible()
})
test('desktop: text file renders in centered dialog', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop', 'desktop only')
await signUpAndGoToFiles(page)
const fileRows = page.locator('table tbody tr')
const count = await fileRows.count()
if (count === 0) {
test.skip(true, 'no files available to preview')
}
await setupPreviewMocks(page, { name: 'notes.txt', type: 'text/plain', size: 50, fixture: 'sample.txt' })
await page.reload()
await expect(page.getByText('notes.txt')).toBeVisible({ timeout: 10000 })
const firstFileRow = fileRows.first()
await firstFileRow.locator('button').last().click()
await page.getByRole('button', { name: 'notes.txt' }).click()
const previewItem = page.getByRole('menuitem', { name: /preview|open/i })
if (!(await previewItem.isVisible({ timeout: 2000 }).catch(() => false))) {
test.skip(true, 'no previewable file found')
}
await previewItem.click()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10000 })
await expect(dialog.locator('p', { hasText: 'notes.txt' })).toBeVisible()
await expect(dialog.getByText('Hello, this is a plain text file')).toBeVisible({ timeout: 10000 })
const previewDialog = page.getByRole('dialog')
await expect(previewDialog).toBeVisible({ timeout: 5000 })
const bounds = await previewDialog.boundingBox()
// Desktop: dialog should NOT be full screen
const bounds = await dialog.boundingBox()
if (bounds) {
const viewport = page.viewportSize()!
expect(bounds.width).toBeLessThan(viewport.width * 0.95)
}
})
})
test('mobile: preview has no horizontal overflow', async ({ page }, testInfo) => {
// ---------------------------------------------------------------------------
// No overflow
// ---------------------------------------------------------------------------
test.describe('Preview no overflow', () => {
test('mobile: page has no horizontal overflow', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile', 'mobile only')
await signUpAndGoToFiles(page)