Files
archy/neode-ui/e2e/viewer-modal.spec.ts
T
archipelago 68a1c14ccf ui: sequence delayed viewer Back events and verify responsive modal layout
Wait for the actual Back event across viewer instances, cancel queued entries when their owner closes, and remove the 250ms assumption. Restore scoped content-size resets so legacy global lightbox CSS cannot narrow the landscape panel.

Three delayed-history unit regressions pass, including a ten-second delay. Five Chromium browser checks pass at 360x800, 390x844, touch landscape 844x390 and desktop 1440x900, plus real local video playback/native fullscreen with source and position retained. Screenshots inspected. Fixture server uses explicit cwd and no filesystem watching; no node proxy. Strict integrated project types and physical Companion UAT remain separate.
2026-11-09 16:51:47 -05:00

82 lines
5.0 KiB
TypeScript

import { test, expect } from '@playwright/test'
test.beforeEach(async ({ page }) => {
await page.route('**/*', route => (new URL(route.request().url()).hostname === '127.0.0.1' || route.request().url().startsWith('data:')) ? route.continue() : route.abort())
})
for (const viewport of [{ width: 360, height: 800 }, { width: 390, height: 844 }, { width: 844, height: 390 }, { width: 1440, height: 900 }]) {
test.describe(`${viewport.width}x${viewport.height}`, () => {
test.use({ viewport, hasTouch: viewport.width <= 844 })
test(`modal layout, focus, fullscreen fallback and Back at ${viewport.width}x${viewport.height}`, async ({ page }) => {
await page.setViewportSize(viewport)
await page.goto('/e2e/fixtures/viewer.html')
// Exercise the embedded-WebView denial branch without requiring native OS fullscreen.
await page.evaluate(() => { HTMLElement.prototype.requestFullscreen = () => Promise.reject(new Error('Embedded browser denied fullscreen')) })
await page.locator('#open-image').click()
const dialog = page.getByRole('dialog', { name: 'File viewer', exact: true })
await expect(dialog).toBeVisible()
const close = page.getByRole('button', { name: 'Close viewer', exact: true })
await expect(close).toBeFocused()
const panel = await dialog.locator('.lightbox-panel').boundingBox()
const content = await dialog.locator('.lightbox-content').boundingBox()
expect(Math.abs(panel!.x + panel!.width / 2 - content!.x - content!.width / 2)).toBeLessThan(2)
for (const label of ['Close viewer', 'Fullscreen', 'Previous file', 'Next file']) {
const box = await page.getByRole('button', { name: label, exact: true }).boundingBox()
expect(box).not.toBeNull()
expect(box!.width).toBeGreaterThanOrEqual(44)
expect(box!.height).toBeGreaterThanOrEqual(44)
expect(box!.x).toBeGreaterThanOrEqual(0)
expect(box!.y).toBeGreaterThanOrEqual(24)
expect(box!.x + box!.width).toBeLessThanOrEqual(viewport.width)
expect(box!.y + box!.height).toBeLessThanOrEqual(viewport.height - 20)
}
await page.keyboard.press('Shift+Tab')
expect(await dialog.evaluate(el => el.contains(document.activeElement))).toBe(true)
expect(await page.locator('#app').evaluate(el => el.inert)).toBe(true)
await page.getByRole('button', { name: 'Fullscreen', exact: true }).click()
await expect(dialog).toHaveClass(/lightbox-expanded/)
await page.keyboard.press('Escape')
await expect(dialog).not.toHaveClass(/lightbox-expanded/)
await page.screenshot({ path: `${process.env.VIEWER_SCREENSHOTS ?? '/tmp'}/viewer-${viewport.width}x${viewport.height}.png` })
await page.goBack()
await expect(dialog).toHaveCount(0)
await expect(page.locator('#open-image')).toBeFocused()
expect(await page.locator('#app').evaluate(el => el.inert)).toBe(false)
// Reopening/explicit close does not navigate away or consume a later viewer's entry.
await page.locator('#open-document').click()
await expect(dialog).toContainText('Invoice with a long filename.pdf')
await expect(dialog.locator('iframe, embed, object')).toHaveCount(0)
await dialog.getByRole('button', { name: 'Download', exact: true }).click()
await expect(page.locator('#download-result')).toHaveText('/document')
await close.click()
await expect(dialog).toHaveCount(0)
await page.locator('#open-image').click()
await page.goBack()
await expect(dialog).toHaveCount(0)
expect(page.url()).toContain('/e2e/fixtures/viewer.html')
})
})
}
test('real video playback retains controls, source and position through fullscreen changes', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/e2e/fixtures/viewer.html')
await page.locator('#open-video').click()
const video = page.locator('.lightbox-media-video')
await video.evaluate(async (node: HTMLVideoElement) => { node.muted = true; await node.play() })
await expect.poll(() => video.evaluate((node: HTMLVideoElement) => node.currentTime)).toBeGreaterThan(0)
const initial = await video.evaluate((node: HTMLVideoElement) => ({ src: node.currentSrc, time: node.currentTime }))
expect(await video.getAttribute('playsinline')).not.toBeNull()
const controls = await video.boundingBox()
const fullscreen = await page.getByRole('button', { name: 'Fullscreen', exact: true }).boundingBox()
expect(controls!.y + controls!.height).toBeLessThanOrEqual(fullscreen!.y)
await page.getByRole('button', { name: 'Fullscreen', exact: true }).click()
await expect(page.getByRole('button', { name: 'Exit fullscreen', exact: true })).toBeVisible()
await page.getByRole('button', { name: 'Exit fullscreen', exact: true }).click()
await expect(page.getByRole('button', { name: 'Fullscreen', exact: true })).toBeVisible()
expect(await video.evaluate((node: HTMLVideoElement) => node.currentSrc)).toBe(initial.src)
expect(await video.evaluate((node: HTMLVideoElement) => node.currentTime)).toBeGreaterThanOrEqual(initial.time)
await page.getByRole('button', { name: 'Close viewer', exact: true }).click()
await expect(video).toHaveCount(0)
})