Files
pardis/tests/e2e/sync.spec.js
Adrien W. 5686ee5072
All checks were successful
Tests et déploiement / validate (push) Successful in 2m44s
Tests et déploiement / deploy (push) Successful in 51s
Added optionnal online storage and cross-client sync
2026-10-10 16:01:06 +02:00

134 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test';
// Helpers only touch synthetic data in fresh browser contexts.
async function store(page, name, operation, value = null) {
return page.evaluate(({ name, operation, value }) => new Promise((resolve, reject) => {
const request = indexedDB.open('TimeLogDB');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const database = request.result;
const tx = database.transaction(name, operation === 'getAll' ? 'readonly' : 'readwrite');
const table = tx.objectStore(name);
const query = operation === 'getAll' ? table.getAll() : operation === 'put' ? table.put(value) : table.clear();
let result;
query.onsuccess = () => { result = query.result; };
tx.oncomplete = () => { database.close(); resolve(result); };
tx.onabort = () => { database.close(); reject(tx.error); };
};
}), { name, operation, value });
}
async function ready(page) {
await page.goto('/#/sync');
await expect(page.getByRole('heading', { name: 'Synchronisation et appareils' })).toBeVisible();
await store(page, 'settings', 'put', { key: 'app', value: { autoStartDay: false } });
// A real clock is necessary for signed HTTP requests.
}
async function create(page) {
await ready(page);
await page.getByLabel('Nom de cet appareil', { exact: true }).fill('Téléphone test');
await page.getByRole('button', { name: 'Préparer la synchronisation', exact: true }).click();
await page.getByText('Afficher le document pour le copier', { exact: true }).click();
const recovery = await page.getByLabel('Contenu du document de secours').inputValue();
await page.getByRole('checkbox', { name: 'J’ai conservé une copie du document à part' }).check();
await page.getByRole('button', { name: 'Activer la synchronisation', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Appareils autorisés', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'Synchroniser maintenant', exact: true })).toBeEnabled();
await expect(page.locator('fieldset')).not.toContainText('Opération impossible');
return recovery;
}
async function sync(page) {
await page.getByRole('button', { name: 'Synchroniser maintenant', exact: true }).click();
await expect(page.getByRole('button', { name: 'Synchroniser maintenant', exact: true })).toBeEnabled();
await expect(page.getByText('Modifications envoyées', { exact: true })).toBeVisible();
}
async function pair(a, b) {
await a.getByRole('button', { name: 'Ajouter un appareil', exact: true }).click();
await expect(a.locator('.code')).toBeVisible();
const code = await a.locator('.code').innerText();
await ready(b);
await b.getByLabel('Code d’association', { exact: true }).fill(code);
await b.getByRole('button', { name: 'Demander la liaison', exact: true }).click();
await expect(b.locator('.fingerprint')).toBeVisible();
await expect(a.locator('.fingerprint')).toBeVisible({ timeout: 15000 });
expect(await a.locator('.fingerprint').innerText()).toBe(await b.locator('.fingerprint').innerText());
await a.getByLabel('Nom du nouvel appareil', { exact: true }).fill('Ordinateur test');
await a.getByRole('checkbox', { name: 'Les deux empreintes sont identiques' }).check();
await a.getByRole('button', { name: 'Autoriser cet appareil', exact: true }).click();
await expect(b.getByRole('heading', { name: 'Appareils autorisés', exact: true })).toBeVisible({ timeout: 15000 });
await sync(b);
}
async function addDay(page, task) {
const date = '2026-10-08', start = Date.parse(`${date}T08:00:00Z`), end = start + 3600000;
await store(page, 'days', 'put', { date, status: 'closed', startedTs: start, endedTs: end, plannedStartTs: start, plannedEndTs: end, lunchType: null, restaurant: null, closePromptedTs: null });
await store(page, 'entries', 'put', { date, seq: 0, kind: 'work', task, client: 'Client synthétique', startTs: start, endTs: end });
}
async function editTask(page, task) {
const entry = (await store(page, 'entries', 'getAll'))[0];
await store(page, 'entries', 'put', { ...entry, task });
}
test('association, modification hors ligne, conflit, rôle et révocation entre deux appareils', async ({ page, browser }) => {
test.setTimeout(90000);
const errors = []; page.on('pageerror', error => errors.push(error.message));
const otherContext = await browser.newContext({ baseURL: 'http://127.0.0.1:4183', viewport: { width: 320, height: 640 } });
const other = await otherContext.newPage(); other.on('pageerror', error => errors.push(error.message));
try {
await create(page); await addDay(page, 'Version initiale'); await sync(page);
await pair(page, other);
expect((await store(other, 'entries', 'getAll'))[0].task).toBe('Version initiale');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(await other.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await other.evaluate(() => navigator.serviceWorker.ready.then(() => true));
await page.context().setOffline(true); await otherContext.setOffline(true);
await other.reload();
await expect(other.getByRole('heading', { name: 'Appareils autorisés', exact: true })).toBeVisible();
await editTask(page, 'Modification téléphone'); await editTask(other, 'Modification ordinateur');
await page.context().setOffline(false); await sync(page);
await otherContext.setOffline(false); await sync(other); await sync(page);
await expect(page.getByRole('heading', { name: 'Versions à départager (1)', exact: true })).toBeVisible();
await expect(page.locator('.conflict')).toContainText('Modification téléphone');
await expect(page.locator('.conflict')).toContainText('Modification ordinateur');
const remote = page.locator('.version').filter({ hasText: 'Modification ordinateur' });
await remote.getByRole('button', { name: 'Conserver cette version', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Versions à départager (0)', exact: true })).toBeVisible();
await sync(other);
expect((await store(page, 'entries', 'getAll'))[0].task).toBe('Modification ordinateur');
expect((await store(other, 'entries', 'getAll'))[0].task).toBe('Modification ordinateur');
await other.getByRole('button', { name: 'Utiliser cet appareil pour les automatismes', exact: true }).click();
await expect(other.getByRole('button', { name: 'Utiliser cet appareil pour les automatismes', exact: true })).toHaveCount(0);
await sync(page);
await expect(page.getByRole('button', { name: 'Utiliser cet appareil pour les automatismes', exact: true })).toBeVisible();
await page.getByRole('button', { name: 'Retirer cet appareil…', exact: true }).click();
await page.getByRole('button', { name: 'Confirmer le retrait', exact: true }).click();
await expect(page.getByRole('button', { name: 'Synchroniser maintenant', exact: true })).toBeEnabled();
await other.getByRole('button', { name: 'Synchroniser maintenant', exact: true }).click();
await expect(other.getByText('Cet appareil a été révoqué.', { exact: true })).toBeVisible();
expect((await store(other, 'entries', 'getAll'))[0].task).toBe('Modification ordinateur');
expect(errors).toEqual([]);
} finally { await otherContext.close(); }
});
test('secours après perte des appareils et annulation de l’invitation en quittant l’écran', async ({ page, browser }) => {
test.setTimeout(60000);
const recovery = await create(page); await addDay(page, 'Historique récupérable'); await sync(page);
await page.getByRole('button', { name: 'Ajouter un appareil', exact: true }).click();
await expect(page.locator('.code')).toBeVisible(); const code = await page.locator('.code').innerText();
await page.getByRole('button', { name: 'Retour aux données', exact: true }).click();
const context = await browser.newContext({ baseURL: 'http://127.0.0.1:4183' }); const replacement = await context.newPage();
try {
await ready(replacement); await replacement.getByLabel('Code d’association', { exact: true }).fill(code);
await replacement.getByRole('button', { name: 'Demander la liaison', exact: true }).click();
await expect(replacement.getByRole('alert')).toContainText('expirée');
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(page.getByText('Une demande est arrivée.', { exact: false })).toHaveCount(0);
await replacement.getByLabel('Ou coller le document').fill(recovery);
await replacement.getByRole('button', { name: 'Récupérer l’accès', exact: true }).click();
await expect(replacement.getByRole('heading', { name: 'Appareils autorisés', exact: true })).toBeVisible();
await sync(replacement);
expect((await store(replacement, 'entries', 'getAll'))[0].task).toBe('Historique récupérable');
const config = (await store(replacement, 'syncState', 'getAll')).find(r => r.key === 'config').value;
expect(config.ring.recovery.privateKey).toBeUndefined();
expect(config.ring.recovery.wrapPrivate).toBeUndefined();
} finally { await context.close(); }
});