mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
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:
@@ -0,0 +1 @@
|
||||
{"hello": "world"}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Sample Markdown
|
||||
|
||||
This is a **test** file.
|
||||
@@ -0,0 +1 @@
|
||||
Hello, this is a plain text file for E2E testing.
|
||||
+119
-44
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user