Added tests
All checks were successful
Tests et déploiement / validate (push) Successful in 2m6s
Tests et déploiement / deploy (push) Successful in 26s

This commit is contained in:
2026-10-10 12:30:13 +02:00
parent 485116f2da
commit 6183ecb181

View File

@@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { readFile } from 'node:fs/promises';
const date = '2026-10-08'; const date = '2026-10-08';
const at = (time, key = date) => Date.parse(`${key}T${time}:00+02:00`); const at = (time, key = date) => Date.parse(`${key}T${time}:00+02:00`);
const now = at('18:00'); const now = at('18:00');
@@ -33,6 +34,25 @@ async function entries(page) {
})); }));
} }
async function storedData(page) {
return page.evaluate(() => new Promise((resolve,reject) => {
const request = indexedDB.open('TimeLogDB');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const stores = ['days','entries','settings'];
const transaction = db.transaction(stores);
const snapshot = {};
for (const store of stores) {
const query = transaction.objectStore(store).getAll();
query.onsuccess = () => { snapshot[store] = query.result; };
}
transaction.oncomplete = () => { db.close(); resolve(snapshot); };
transaction.onabort = () => { db.close(); reject(transaction.error); };
};
}));
}
test('consultation, correction, recalcul, copie et statistiques à 320 px', async ({page,context}) => { test('consultation, correction, recalcul, copie et statistiques à 320 px', async ({page,context}) => {
await context.grantPermissions(['clipboard-read','clipboard-write']); await context.grantPermissions(['clipboard-read','clipboard-write']);
await importFixture(page); await importFixture(page);
@@ -126,6 +146,36 @@ test('écran données dédié, conservation persistée et accès depuis le rappe
await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible(); await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible();
}); });
test('échec de conservation restaure le champ et les données, puis permet de réessayer', async ({page}) => {
await importFixture(page,fixture({dates:['2025-01-01',date]}));
await page.getByRole('button',{name:'Sauvegarder',exact:true}).click();
const input = page.getByLabel('Conserver (jours)');
await expect(input).toHaveValue('30');
const before = await storedData(page);
await page.evaluate(() => {
const originalPut = IDBObjectStore.prototype.put;
IDBObjectStore.prototype.put = function(record,...args) {
if (this.name === 'settings' && record.key === 'app') {
IDBObjectStore.prototype.put = originalPut;
throw new Error('Échec de conservation simulé');
}
return originalPut.call(this,record,...args);
};
});
await input.fill('60'); await input.press('Tab');
await expect(page.getByRole('alert')).toContainText('Enregistrement impossible');
await expect(input).toHaveValue('30');
expect(await storedData(page)).toEqual(before);
await page.reload();
await expect(input).toHaveValue('30');
await input.fill('60'); await input.press('Tab');
await expect.poll(async () => (await storedData(page)).settings.find(row => row.key === 'app').value.retentionDays).toBe(60);
await expect(page.getByRole('alert')).toHaveCount(0);
const after = await storedData(page);
expect(after.days).toEqual(before.days);
expect(after.entries).toEqual(before.entries);
});
test('reprise annulée puis concurrente entre deux onglets, choix pause par défaut', async ({page,context}) => { test('reprise annulée puis concurrente entre deux onglets, choix pause par défaut', async ({page,context}) => {
await importFixture(page,fixture({pause:false})); await importFixture(page,fixture({pause:false}));
const before = await entries(page); const before = await entries(page);
@@ -184,7 +234,7 @@ test('export relu et vérifié, PWA relancée hors ligne', async ({page,context}
const download = await downloading; const download = await downloading;
await page.getByRole('button',{name:'Vérifier le fichier exporté…'}).click(); await page.getByRole('button',{name:'Vérifier le fichier exporté…'}).click();
await page.locator('input[type=file]').setInputFiles(await download.path()); await page.locator('input[type=file]').setInputFiles(await download.path());
await expect(page.getByRole('status').filter({hasText:'Sauvegarde vérifiée'})).toBeVisible(); await expect(page.getByRole('status').filter({hasText:/^Sauvegarde vérifiée :/})).toBeVisible();
await page.evaluate(async () => { await navigator.serviceWorker.ready; }); await page.evaluate(async () => { await navigator.serviceWorker.ready; });
await page.reload(); await page.reload();
await expect.poll(() => page.evaluate(() => Boolean(navigator.serviceWorker.controller))).toBe(true); await expect.poll(() => page.evaluate(() => Boolean(navigator.serviceWorker.controller))).toBe(true);
@@ -195,6 +245,35 @@ test('export relu et vérifié, PWA relancée hors ligne', async ({page,context}
await expect(page.locator('.total')).toContainText('8h30'); await expect(page.locator('.total')).toContainText('8h30');
}); });
test('export modifié refusé sans écriture, puis vérification du fichier original', async ({page}) => {
await importFixture(page);
await page.getByRole('button',{name:'Sauvegarder',exact:true}).click();
const downloading = page.waitForEvent('download');
await page.getByRole('button',{name:'Exporter les données',exact:true}).click();
const download = await downloading;
const original = await readFile(await download.path());
const modified = JSON.parse(original.toString('utf8'));
modified.entries[0].task = 'Tâche modifiée dans le fichier';
await expect(page.getByRole('status').filter({hasText:'Export préparé'})).toBeVisible();
const before = await storedData(page);
await page.getByRole('button',{name:'Vérifier le fichier exporté…'}).click();
await page.locator('input[type=file]').setInputFiles({
name:'export-modifie.json',mimeType:'application/json',buffer:Buffer.from(JSON.stringify(modified)),
});
await expect(page.getByRole('alert')).toContainText('Ce fichier ne correspond pas au dernier export préparé');
expect(await storedData(page)).toEqual(before);
await expect(page.getByRole('status').filter({hasText:/^Sauvegarde vérifiée :/})).toHaveCount(0);
await page.getByRole('button',{name:'Vérifier le fichier exporté…'}).click();
await page.locator('input[type=file]').setInputFiles(await download.path());
await expect(page.getByRole('status').filter({hasText:/^Sauvegarde vérifiée :/})).toBeVisible();
await expect(page.getByRole('alert')).toHaveCount(0);
const after = await storedData(page);
expect(after.days).toEqual(before.days);
expect(after.entries).toEqual(before.entries);
expect(after.settings.find(row => row.key === 'backup-status').value.verifiedExportedAt)
.toBe(before.settings.find(row => row.key === 'backup-status').value.exportedAt);
});
test('échec IndexedDB visible et reprise avec Réessayer', async ({page}) => { test('échec IndexedDB visible et reprise avec Réessayer', async ({page}) => {
await page.addInitScript(() => { await page.addInitScript(() => {
const original = indexedDB.open.bind(indexedDB); const original = indexedDB.open.bind(indexedDB);
@@ -227,6 +306,49 @@ test('purge confirmée et restauration de sa sauvegarde récupérable', async ({
expect(await entries(page)).toHaveLength(6); expect(await entries(page)).toHaveLength(6);
}); });
test('échec de purge conserve les journées et la sauvegarde précédente, puis permet de réessayer', async ({page}) => {
await importFixture(page,fixture({dates:['2025-01-01',date]}));
await page.getByRole('button',{name:'Sauvegarder',exact:true}).click();
const dialog = page.getByRole('dialog',{name:'Confirmer la purge'});
await page.getByRole('button',{name:'Purger maintenant'}).click();
await dialog.getByRole('button',{name:'Confirmer la purge'}).click();
await expect(dialog).toHaveCount(0);
await page.getByRole('button',{name:'Récupérer la sauvegarde avant purge'}).click();
await page.getByRole('button',{name:'Fusionner',exact:true}).click();
await expect(page.getByRole('status').filter({hasText:'1 journée ajoutée'})).toBeVisible();
const before = await storedData(page);
expect(before.days).toHaveLength(2);
expect(before.entries).toHaveLength(6);
expect(before.settings.find(row => row.key === 'before-purge')).toBeDefined();
// Make the next backup differ even if the restored records are identical.
await page.clock.runFor(1000);
await page.getByRole('button',{name:'Purger maintenant'}).click();
await page.evaluate(() => {
const originalDelete = IDBObjectStore.prototype.delete;
IDBObjectStore.prototype.delete = function(...args) {
if (this.name === 'days') {
IDBObjectStore.prototype.delete = originalDelete;
// Entries and the new backup have already been written in this transaction.
throw new Error('Échec de purge simulé');
}
return originalDelete.apply(this,args);
};
});
await dialog.getByRole('button',{name:'Confirmer la purge'}).click();
await expect(dialog.getByRole('alert')).toContainText('Échec de purge simulé');
expect(await storedData(page)).toEqual(before);
await expect(dialog.getByRole('button',{name:'Confirmer la purge'})).toBeEnabled();
await dialog.getByRole('button',{name:'Confirmer la purge'}).click();
await expect(dialog).toHaveCount(0);
const after = await storedData(page);
expect(after.days).toEqual(before.days.filter(day => day.date === date));
expect(after.entries).toEqual(before.entries.filter(entry => entry.date === date));
const recovered = after.settings.find(row => row.key === 'before-purge').value;
expect(recovered.days.map(day => day.date).sort()).toEqual(['2025-01-01',date]);
expect(recovered.entries).toHaveLength(6);
expect(recovered.exportedAt).not.toBe(before.settings.find(row => row.key === 'before-purge').value.exportedAt);
});
test('dialogue horaire imbriqué garde le focus et le rend à la correction', async ({page}) => { test('dialogue horaire imbriqué garde le focus et le rend à la correction', async ({page}) => {
await importFixture(page); await importFixture(page);
await page.getByRole('button',{name:'Voir les détails'}).click(); await page.getByRole('button',{name:'Voir les détails'}).click();