Les tableurs étaient invisibles à la recherche, chaque saisie devenait du texte, et une formule n'affichait que sa formule. - A5 : extract_indexable_text() indexe les noms de feuilles et les 20 premières lignes (plafond 5 k caractères) pour le TF-IDF et la recherche sémantique. Un mot tapé dans une cellule rend le fichier trouvable ; un classeur chiffré s'indexe par son seul nom. - A10 : la saisie est typée comme dans Excel — booléens (TRUE/FAUX/OUI/NON) et dates FR JJ/MM/AAAA en ordre jour-first, donc 01/02/2026 est le 1er février. Une saisie ressemblant à une formule n'est jamais convertie (BUG-088 préservé). - A12 : la valeur calculée par Excel s'affiche sous la formule, via une 2e lecture data_only=True faite seulement si l'archive contient un <v>. Info-bulle traduite FR/EN, aucun texte d'interface côté backend. - BUG-089 : un reindex manuel ne reconstruisait pas l'index inversé, et backend/search.py lisait l'index via un import par valeur — après un rechargement du module, la recherche écrivait dans un dict périmé. Contre-preuves vérifiées pour A5, A10 et A12 (neutralisation de chaque fonction → échec des tests concernés). Tests : 1402 pytest, 10 JSDOM, 4 E2E, suite E2E complète verte, ruff/mypy 0. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <[email protected]>
144 lines
5.8 KiB
JavaScript
144 lines
5.8 KiB
JavaScript
/**
|
|
* E2E tests — Excel viewer, write guards (ROADMAP #153 P0).
|
|
*
|
|
* Fixture : `test_vault/sample-xlsx-lossy.xlsx` — a plain 2x2 workbook whose
|
|
* sheet XML carries a cached formula result (`<f>B1*2</f><v>200</v>`) and whose
|
|
* package contains `xl/slicers/slicer1.xml`. Both are dropped by an openpyxl
|
|
* round-trip, so the read response must report
|
|
* `xlsx_lossy_features: ["cached_values", "slicers"]` (BUG-085 A1).
|
|
*
|
|
* Covered :
|
|
* - the warning banner lists both features ;
|
|
* - saving a cell on that workbook asks for confirmation (native dialog) and
|
|
* then succeeds (the client retries with `force: true`) ;
|
|
* - the f(x) toggle is off by default, so "=B1*3" is stored as text ;
|
|
* - the value Excel last computed is shown under the formula (#153 A12).
|
|
*
|
|
* The fixture is restored byte-for-byte in `afterAll` so a local run never
|
|
* dirties the working copy.
|
|
*
|
|
* Run (local) : BASE_URL=http://localhost:2029 npx playwright test tests/e2e/xlsx-viewer.spec.js
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:2029';
|
|
const VAULT = 'TestVault';
|
|
const FIXTURE = 'sample-xlsx-lossy.xlsx';
|
|
// Playwright runs from the repository root (run-e2e-local.* / CI both do).
|
|
const FIXTURE_PATH = path.resolve(process.cwd(), 'test_vault', FIXTURE);
|
|
|
|
let originalBytes = null;
|
|
|
|
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', process.env.OBSIGATE_USER || 'admin');
|
|
await page.fill('#login-password', process.env.OBSIGATE_PASS || 'test123');
|
|
await page.click('#login-btn');
|
|
}
|
|
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
|
|
}
|
|
|
|
async function openFixture(page) {
|
|
const treeItem = page.locator(`.tree-item[data-vault="${VAULT}"][data-path="${FIXTURE}"]`);
|
|
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 });
|
|
await expect(page.locator('#content-area .xlsx-table')).toBeVisible({ timeout: 15000 });
|
|
}
|
|
|
|
test.describe('Excel viewer — garde-fous d\'écriture et valeurs calculées (#153)', () => {
|
|
test.beforeAll(() => {
|
|
if (existsSync(FIXTURE_PATH)) originalBytes = readFileSync(FIXTURE_PATH);
|
|
});
|
|
|
|
test.afterAll(() => {
|
|
if (originalBytes) writeFileSync(FIXTURE_PATH, originalBytes);
|
|
});
|
|
|
|
// Read-only assertions come FIRST, before the mutating tests: saving through
|
|
// the viewer rewrites the workbook and an openpyxl round-trip drops the cached
|
|
// formula results (BUG-085), so the shadow line only exists on a pristine
|
|
// fixture.
|
|
test('affiche la valeur calculée en cache sous la formule (#153 A12)', async ({ page }) => {
|
|
await login(page);
|
|
await openFixture(page);
|
|
|
|
// B2 is "=B1*2" and the package keeps its last result (<v>200</v>).
|
|
const formulaCell = page.locator('#content-area td[data-cell="B2"]');
|
|
await expect(formulaCell).toContainText('=B1*2');
|
|
|
|
const cached = formulaCell.locator('.xlsx-cached');
|
|
await expect(cached).toHaveCount(1);
|
|
await expect(cached).toHaveText('200');
|
|
// The tooltip is translated client-side, never hardcoded by the backend.
|
|
await expect(cached).toHaveAttribute('title', /Excel/);
|
|
|
|
// A plain value cell must not be duplicated with a shadow line.
|
|
await expect(page.locator('#content-area td[data-cell="B1"] .xlsx-cached')).toHaveCount(0);
|
|
});
|
|
|
|
test('affiche la bannière listant les éléments non préservés', async ({ page }) => {
|
|
await login(page);
|
|
await openFixture(page);
|
|
|
|
const banner = page.locator('#content-area .xlsx-warning');
|
|
await expect(banner).toBeVisible();
|
|
// 2 features : valeurs calculées + segments (jamais de couleur codée en dur,
|
|
// les libellés viennent bien des locales).
|
|
await expect(banner.locator('.xlsx-warning-tag')).toHaveCount(2);
|
|
await expect(banner).toContainText('segments');
|
|
await expect(banner).toContainText('valeurs calculées');
|
|
});
|
|
|
|
test('demande confirmation puis enregistre la cellule', async ({ page }) => {
|
|
await login(page);
|
|
await openFixture(page);
|
|
|
|
let dialogMessage = null;
|
|
page.on('dialog', async (dialog) => {
|
|
dialogMessage = dialog.message();
|
|
await dialog.accept();
|
|
});
|
|
|
|
const cell = page.locator('#content-area td[data-cell="A2"]');
|
|
await cell.click();
|
|
await cell.fill('Total confirmé');
|
|
await cell.press('Enter');
|
|
|
|
const save = page.locator('#xlsx-save-btn');
|
|
await expect(save).toBeEnabled();
|
|
await save.click();
|
|
|
|
await expect.poll(() => dialogMessage, { timeout: 10000 }).toContain('segments');
|
|
await expect(page.locator('.toast-success')).toBeVisible({ timeout: 10000 });
|
|
|
|
// La cellule reste modifiée côté UI (plus de marque « sale »).
|
|
await expect(page.locator('#content-area td.xlsx-dirty')).toHaveCount(0);
|
|
});
|
|
|
|
test('le toggle f(x) est désactivé par défaut (formule stockée en texte)', async ({ page }) => {
|
|
await login(page);
|
|
await openFixture(page);
|
|
|
|
const toggle = page.locator('#xlsx-formula-btn');
|
|
await expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
|
|
|
// 409 → confirmation, puis reprise avec force (le toggle reste désactivé).
|
|
page.on('dialog', (dialog) => dialog.accept());
|
|
const cell = page.locator('#content-area td[data-cell="B2"]');
|
|
await cell.click();
|
|
await cell.fill('=B1*3');
|
|
await cell.press('Enter');
|
|
await page.locator('#xlsx-save-btn').click();
|
|
await expect(page.locator('.toast-success')).toBeVisible({ timeout: 10000 });
|
|
});
|
|
});
|