Added tests
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
const date = '2026-10-08';
|
||||
const at = (time, key = date) => Date.parse(`${key}T${time}:00+02: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}) => {
|
||||
await context.grantPermissions(['clipboard-read','clipboard-write']);
|
||||
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();
|
||||
});
|
||||
|
||||
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}) => {
|
||||
await importFixture(page,fixture({pause:false}));
|
||||
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;
|
||||
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('status').filter({hasText:/^Sauvegarde vérifiée :/})).toBeVisible();
|
||||
await page.evaluate(async () => { await navigator.serviceWorker.ready; });
|
||||
await page.reload();
|
||||
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');
|
||||
});
|
||||
|
||||
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}) => {
|
||||
await page.addInitScript(() => {
|
||||
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);
|
||||
});
|
||||
|
||||
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}) => {
|
||||
await importFixture(page);
|
||||
await page.getByRole('button',{name:'Voir les détails'}).click();
|
||||
|
||||
Reference in New Issue
Block a user