Files
zpan/e2e/responsive-preview.spec.ts
T
Jasper Van ad0f21bb39 fix: unify list pagination and realtime updates (#524)
* fix!: unify pagination and realtime change delivery

Replace offset paging on affected unbounded collections with signed keyset tokens and infinite loading. Persist scoped resource changes so one global SSE connection can resume and invalidate query caches safely.

BREAKING CHANGE: migrated list APIs now accept pageToken and return nextPageToken instead of page and total fields.

Refs #450

* fix: keep page tokens at the HTTP boundary

Move signed page-token handling out of the pure domain layer so dependency-cruiser architecture checks pass without changing behavior.

* fix: route background job stats through usecase

Keep the HTTP boundary from reaching directly into repository ports and cover the new usecase wrapper.

* fix: align clients and checks with cursor pagination

* refactor: unify pagination boundaries and infinite loading
2026-07-27 02:02:53 -04:00

154 lines
5.6 KiB
TypeScript

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 per file type: mobile drawer vs desktop dialog
// ---------------------------------------------------------------------------
test.describe('Preview with mocked files', () => {
test('mobile: text file renders in full-screen drawer @mobile', async ({ page }) => {
await signUpAndGoToFiles(page)
await setupPreviewMocks(page, { name: 'readme.txt', type: 'text/plain', size: 50, fixture: 'sample.txt' })
// Reload to get mocked file list
await page.reload()
await expect(page.getByText('readme.txt')).toBeVisible({ timeout: 10000 })
// Click file name to open preview
await page.getByRole('button', { name: 'readme.txt' }).click()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible({ timeout: 10000 })
// Header shows file name
await expect(dialog.locator('p', { hasText: 'readme.txt' })).toBeVisible()
// 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)
expect(bounds.width).toBeGreaterThanOrEqual(viewport.width - 2)
}
})
test('mobile: markdown file renders in drawer @mobile', async ({ page }) => {
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 @mobile', async ({ page }) => {
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 @desktop', async ({ page }) => {
await signUpAndGoToFiles(page)
await setupPreviewMocks(page, { name: 'notes.txt', type: 'text/plain', size: 50, fixture: 'sample.txt' })
await page.reload()
await expect(page.getByRole('button', { name: 'notes.txt' })).toBeVisible({ timeout: 10000 })
await page.getByRole('button', { name: 'notes.txt' }).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 })
// 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)
}
})
})
// ---------------------------------------------------------------------------
// No overflow
// ---------------------------------------------------------------------------
test.describe('Preview no overflow', () => {
test('mobile: page has no horizontal overflow @mobile', async ({ page }) => {
await signUpAndGoToFiles(page)
const hasHScroll = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
)
expect(hasHScroll).toBe(false)
})
})