190 lines
8.0 KiB
JavaScript
190 lines
8.0 KiB
JavaScript
/**
|
|
* E2E tests for the ObsiGate image viewer (roadmap #108).
|
|
*
|
|
* Fixtures : `test_vault/sample-image.png` (96x64) + `test_vault/sample-vector.svg`.
|
|
*
|
|
* Run (local):
|
|
* BASE_URL=http://localhost:2029 npx playwright test tests/e2e/image-viewer.spec.js
|
|
* BASE_URL=http://localhost:2029 npx playwright test tests/e2e/image-viewer.spec.js --headed
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:2029';
|
|
|
|
const CREDS = {
|
|
username: process.env.OBSIGATE_USER || 'admin',
|
|
password: process.env.OBSIGATE_PASS || 'test123',
|
|
};
|
|
|
|
async function login(page) {
|
|
await page.goto(BASE);
|
|
const loginForm = page.locator('#login-screen');
|
|
await expect(loginForm).toBeVisible({ timeout: 5000 }).catch(() => {});
|
|
if (await loginForm.isVisible()) {
|
|
await page.fill('#login-username', CREDS.username);
|
|
await page.fill('#login-password', CREDS.password);
|
|
await page.click('#login-btn');
|
|
}
|
|
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
|
|
}
|
|
|
|
async function openFile(page, vault, filePath) {
|
|
const treeItem = page.locator(`.tree-item[data-vault="${vault}"][data-path="${filePath}"]`);
|
|
if (!(await treeItem.count())) {
|
|
await page.locator(`.tree-item.vault-item[data-vault="${vault}"]`).first().click();
|
|
await treeItem.waitFor({ state: 'attached', timeout: 8000 });
|
|
}
|
|
await treeItem.dblclick({ timeout: 5000 });
|
|
}
|
|
|
|
test.describe('Image viewer — zoom / pan / navigation (#108)', () => {
|
|
|
|
test('affiche l\'image dans la visionneuse dédiée (URL /api/image)', async ({ page }) => {
|
|
// #108-B1 — l'image isolée doit pointer vers /api/image (octets), pas /raw (JSON).
|
|
const imageResponsePromise = page.waitForResponse(
|
|
(r) => r.url().includes('/api/image/') && r.status() === 200,
|
|
{ timeout: 15000 },
|
|
);
|
|
|
|
await login(page);
|
|
await openFile(page, 'TestVault', 'sample-image.png');
|
|
|
|
const main = page.locator('#content-area .image-viewer-container img.image-main');
|
|
await expect(main).toBeVisible({ timeout: 10000 });
|
|
await expect(main).toHaveAttribute('src', /\/api\/image\/TestVault\?path=/);
|
|
|
|
const resp = await imageResponsePromise;
|
|
expect(resp.headers()['content-type']).toContain('image/png');
|
|
|
|
// Le badge de zoom démarre à 100 %.
|
|
await expect(page.locator('#content-area .image-zoom-badge')).toHaveText('100%');
|
|
});
|
|
|
|
test('le zoom molette et le reset modifient la transform', async ({ page }) => {
|
|
await login(page);
|
|
await openFile(page, 'TestVault', 'sample-image.png');
|
|
await expect(page.locator('#content-area .image-stage')).toBeVisible({ timeout: 10000 });
|
|
|
|
const badge = page.locator('#content-area .image-zoom-badge');
|
|
await expect(badge).toHaveText('100%');
|
|
|
|
await page.locator('#content-area .image-stage').hover();
|
|
await page.mouse.wheel(0, -240);
|
|
await expect(badge).not.toHaveText('100%', { timeout: 5000 });
|
|
|
|
const transform = await page.locator('#content-area img.image-main').evaluate(
|
|
(el) => getComputedStyle(el).transform,
|
|
);
|
|
expect(transform).not.toBe('none');
|
|
|
|
// Double-clic = réinitialisation.
|
|
await page.locator('#content-area .image-stage').dblclick();
|
|
await expect(badge).toHaveText('100%');
|
|
});
|
|
|
|
test('navigue entre les images du dossier via la pellicule', async ({ page }) => {
|
|
await login(page);
|
|
await openFile(page, 'TestVault', 'sample-image.png');
|
|
|
|
const strip = page.locator('#content-area .image-nav-strip');
|
|
await expect(strip).toBeVisible({ timeout: 10000 });
|
|
// sample-image.png et sample-vector.svg partagent le dossier racine.
|
|
await expect(strip.locator('img.image-thumb')).toHaveCount(2);
|
|
|
|
await page.locator('#content-area .image-nav-strip img.image-thumb').first().click();
|
|
await expect(page.locator('#content-area .image-title')).toBeVisible();
|
|
});
|
|
|
|
test('conserve le plein écran et le panneau métadonnées à la navigation (#BUG-072)', async ({ page }) => {
|
|
await login(page);
|
|
await openFile(page, 'TestVault', 'sample-image.png');
|
|
|
|
const container = page.locator('#content-area .image-viewer-container');
|
|
const metaPanel = page.locator('#content-area .image-meta-panel');
|
|
const title = page.locator('#content-area .image-title');
|
|
await expect(container).toBeVisible({ timeout: 10000 });
|
|
|
|
// Métadonnées : barre latérale à droite de l'image (pas sous la pellicule).
|
|
await page.locator('#content-area .image-btn-metadata').click();
|
|
await expect(metaPanel).toBeVisible();
|
|
const stageBox = await page.locator('#content-area .image-stage').boundingBox();
|
|
const metaBox = await metaPanel.boundingBox();
|
|
expect(metaBox.x).toBeGreaterThanOrEqual(stageBox.x + stageBox.width - 1);
|
|
|
|
// Plein écran activé.
|
|
await page.locator('#content-area .image-btn-lightbox').click();
|
|
await expect(container).toHaveClass(/lightbox/);
|
|
|
|
// Naviguer (flèche droite) : les deux états doivent survivre au re-render.
|
|
const titleBefore = await title.innerText();
|
|
await page.keyboard.press('ArrowRight');
|
|
await expect(container).toHaveClass(/lightbox/);
|
|
await expect(metaPanel).toBeVisible();
|
|
await expect(title).not.toHaveText(titleBefore);
|
|
});
|
|
|
|
test('navigation en place : flèches latérales, pellicule persistante et cadre ajusté (#111)', async ({ page }) => {
|
|
await login(page);
|
|
await openFile(page, 'TestVault', 'sample-image.png');
|
|
|
|
const container = page.locator('#content-area .image-viewer-container');
|
|
const strip = page.locator('#content-area .image-nav-strip');
|
|
const main = page.locator('#content-area img.image-main');
|
|
await expect(strip).toBeVisible({ timeout: 10000 });
|
|
await expect(strip.locator('img.image-thumb')).toHaveCount(2);
|
|
|
|
// Marqueur : la navigation en place ne doit PAS recréer le conteneur.
|
|
await container.evaluate((el) => { el.dataset.inplace = '1'; });
|
|
|
|
// L'image reste contenue dans le cadre (jamais plus grande).
|
|
const stageBox = await page.locator('#content-area .image-stage').boundingBox();
|
|
const imgBox = await main.boundingBox();
|
|
expect(imgBox.width).toBeLessThanOrEqual(stageBox.width + 1);
|
|
expect(imgBox.height).toBeLessThanOrEqual(stageBox.height + 1);
|
|
|
|
// Flèche superposée révélée au survol.
|
|
const nextArrow = page.locator('#content-area .image-nav-arrow-next');
|
|
await expect(nextArrow).toBeAttached();
|
|
await nextArrow.hover();
|
|
await expect.poll(() => nextArrow.evaluate((el) => getComputedStyle(el).opacity)).toBe('1');
|
|
|
|
const title = page.locator('#content-area .image-title');
|
|
const before = await title.innerText();
|
|
await nextArrow.click();
|
|
|
|
await expect(title).not.toHaveText(before);
|
|
await expect(strip).toBeVisible();
|
|
await expect(strip.locator('img.image-thumb')).toHaveCount(2);
|
|
await expect(container).toHaveAttribute('data-inplace', '1');
|
|
await expect(page.locator('#content-area .image-counter')).toContainText('/');
|
|
});
|
|
|
|
test('la pellicule est défilable (molette + flèches latérales) sans changer d\'image (#112)', async ({ page }) => {
|
|
await login(page);
|
|
await openFile(page, 'TestVault', 'sample-image.png');
|
|
|
|
const nav = page.locator('#content-area .image-nav');
|
|
const strip = page.locator('#content-area .image-nav-strip');
|
|
await expect(strip).toBeVisible({ timeout: 10000 });
|
|
await expect(nav).toBeAttached();
|
|
|
|
// Les deux flèches de défilement existent (translucides, révélées au survol).
|
|
const prev = page.locator('#content-area .image-strip-arrow-prev');
|
|
const next = page.locator('#content-area .image-strip-arrow-next');
|
|
await expect(prev).toBeAttached();
|
|
await expect(next).toBeAttached();
|
|
await expect
|
|
.poll(() => next.evaluate((el) => getComputedStyle(el).opacity))
|
|
.not.toBe('1'); // au repos, la flèche n'est pas pleinement visible
|
|
|
|
// Faire défiler la pellicule ne change PAS l'image courante.
|
|
const title = page.locator('#content-area .image-title');
|
|
const before = await title.innerText();
|
|
await next.hover();
|
|
await next.click();
|
|
await prev.click();
|
|
await expect(title).toHaveText(before);
|
|
});
|
|
});
|