Added tests
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user