148 lines
10 KiB
JavaScript
148 lines
10 KiB
JavaScript
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: 'Créer mon coffre', 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: 'Créer le coffre et 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: 'Choisir les données à conserver', exact: true })).toBeVisible({ timeout: 15000 });
|
||
await b.getByRole('button', { name: 'Tout conserver du coffre existant', 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 page.getByRole('button', { name: 'Fusion manuelle détaillée (1)', exact: true }).click();
|
||
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 page.getByRole('button', { name: 'Retour aux appareils', exact: true }).click();
|
||
await sync(page);
|
||
await expect(page.getByRole('button', { name: 'Utiliser cet appareil pour les automatismes', exact: true })).toBeVisible();
|
||
await page.getByRole('button', { name: 'Retour aux données', exact: true }).click();
|
||
const roleNotice = page.getByText('Les automatismes sont exécutés sur un autre appareil.', { exact: true });
|
||
await expect(roleNotice).toBeVisible();
|
||
await page.getByRole('button', { name: 'Plus tard', exact: true }).click();
|
||
await expect(roleNotice).toHaveCount(0);
|
||
await page.reload();
|
||
await expect(page.getByRole('heading', { name: 'Données et sauvegardes', exact: true })).toBeVisible();
|
||
await expect(roleNotice).toHaveCount(0);
|
||
await page.goto('/#/sync');
|
||
await expect(page.getByRole('heading', { name: 'Synchronisation et appareils', 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(); }
|
||
});
|