Added optionnal online storage and cross-client sync
This commit is contained in:
133
tests/e2e/sync.spec.js
Normal file
133
tests/e2e/sync.spec.js
Normal file
@@ -0,0 +1,133 @@
|
||||
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(); }
|
||||
});
|
||||
Reference in New Issue
Block a user