Files
pardis/tests/e2e/parcours.spec.js
Adrien W. 485116f2da
All checks were successful
Tests et déploiement / validate (push) Successful in 1m55s
Tests et déploiement / deploy (push) Successful in 37s
Nested UI for data & storage options
2026-10-10 12:08:06 +02:00

298 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test';
const date = '2026-10-08';
const at = (time, key = date) => Date.parse(`${key}T${time}:00+02:00`);
const now = at('18:00');
function fixture({ pause = true, dates = [date], lastKind = 'work' } = {}) {
const days = dates.map(key => ({ date:key, status:'closed', startedTs:at('08:00',key), endedTs:at('17:00',key), plannedStartTs:at('08:00',key), plannedEndTs:at('17:00',key), lunchType:'panier' }));
const entry = (key, seq, kind, start, end) => ({ date:key, seq, kind, task:kind === 'break' ? 'Pause' : kind === 'lunch' ? 'Pause déjeuner' : 'Tâche de test', client:kind === 'work' ? 'Client de test' : null, startTs:at(start,key),endTs:at(end,key) });
const entries = dates.flatMap(key => pause ? [entry(key,0,'work','08:00','12:00'),entry(key,1,'break','12:00','12:30'),entry(key,2,'work','12:30','17:00')] : lastKind === 'work' ? [entry(key,0,'work','08:00','17:00')] : [entry(key,0,'work','08:00','16:00'),entry(key,1,lastKind,'16:00','17:00')]);
return { format:'timelog-export',version:2,exportedAt:new Date(now).toISOString(),settings:{autoStartDay:false,autoEndDay:false,autoLunch:false,retentionDays:30,roundingMinutes:15},days,entries };
}
async function importFixture(page, payload = fixture()) {
await page.clock.install({ time:now });
await page.goto('/#/settings');
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
await page.getByRole('button',{name:'Gérer les données et sauvegardes'}).click();
await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible();
await page.locator('input[type=file]').setInputFiles({ name:'fixture-synthetique.json',mimeType:'application/json',buffer:Buffer.from(JSON.stringify(payload)) });
await page.getByRole('button',{name:'Remplacer tout',exact:true}).click();
await page.getByRole('dialog',{name:'Remplacer les données'}).getByRole('button',{name:'Confirmer',exact:true}).click();
await expect(page.getByRole('status').filter({hasText:'Données remplacées'})).toBeVisible();
await page.getByRole('button',{name:'Récap',exact:true}).click();
}
async function entries(page) {
return page.evaluate(() => new Promise((resolve,reject) => {
const request = indexedDB.open('TimeLogDB');
request.onerror = () => reject(new Error('Cannot open test database'));
request.onsuccess = () => {
const db = request.result;
const query = db.transaction('entries').objectStore('entries').getAll();
query.onsuccess = () => { resolve(query.result.sort((a,b)=>a.date.localeCompare(b.date)||a.seq-b.seq)); db.close(); };
query.onerror = () => { reject(new Error('Cannot read test entries')); db.close(); };
};
}));
}
test('consultation, correction, recalcul, copie et statistiques à 320 px', async ({page,context}) => {
await context.grantPermissions(['clipboard-read','clipboard-write']);
await importFixture(page);
await expect(page.locator('.total')).toContainText('8h30');
await expect(page.locator('.screen')).not.toContainText('0h30');
await page.getByRole('button',{name:'Copier le récapitulatif'}).click();
const copied = await page.evaluate(() => navigator.clipboard.readText());
expect(copied).toContain('8h30'); expect(copied).not.toContain('Pause');
await page.getByRole('button',{name:'Voir les détails'}).click();
await expect(page.locator('.timeline button')).toHaveCount(3);
await expect(page.locator('.timeline button').nth(1)).toBeDisabled();
await expect(page.getByRole('dialog')).toHaveCount(0);
await page.getByRole('button',{name:'Corriger cette journée'}).click();
await page.locator('.timeline button').nth(1).click();
const dialog = page.getByRole('dialog',{name:'Modifier le poste'});
await expect(dialog.getByRole('button',{name:'Fermer',exact:true})).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(dialog.getByRole('button',{name:'Supprimer ce poste'})).toBeFocused();
await page.keyboard.press('Tab');
await expect(dialog.getByRole('button',{name:'Fermer',exact:true})).toBeFocused();
await page.keyboard.press('Escape');
await expect(page.locator('.timeline button').nth(1)).toBeFocused();
await page.locator('.timeline button').nth(1).click();
await dialog.getByRole('button',{name:'Supprimer ce poste'}).click();
await dialog.getByRole('button',{name:'Confirmer la suppression'}).click();
await expect(page.locator('.timeline button')).toHaveCount(2);
await page.getByRole('button',{name:'Retour au récapitulatif'}).click();
await expect(page.locator('.total')).toContainText('9h00');
await page.getByRole('button',{name:'Réglages',exact:true}).click();
await page.getByRole('button',{name:'Consulter les statistiques'}).click();
await expect(page.getByTestId('stats-worked')).toHaveText('9h00');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
// Wider or larger fonts must also fit the backup reminder on CI and mobile.
await expect(page.locator('.update')).toContainText('Sauvegarde à vérifier');
await page.addStyleTag({content:'.update { font-family: monospace; font-size: 24px; }'});
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
});
test('date conservée, rechargement et Retour depuis les trois nouvelles pages', async ({page}) => {
await importFixture(page,fixture({dates:['2026-10-07',date]}));
await page.getByLabel('Journée à consulter').selectOption('2026-10-07');
await page.getByRole('button',{name:'Voir les détails'}).click();
await page.reload();
await expect(page.getByRole('heading',{name:/7 octobre/})).toBeVisible();
await page.getByRole('button',{name:'Retour au récapitulatif'}).click();
await expect(page.getByLabel('Journée à consulter')).toHaveValue('2026-10-07');
await page.getByRole('button',{name:'Réglages',exact:true}).click();
await page.getByRole('button',{name:'Gérer les automatismes'}).click();
await expect(page.getByRole('checkbox')).toHaveCount(6);
await page.goBack();
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
await page.getByRole('button',{name:'Gérer les jours non travaillés'}).click();
await expect(page.getByLabel('Date à bloquer')).toBeVisible();
await page.goBack();
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
});
test('écran données dédié, conservation persistée et accès depuis le rappel', async ({page}) => {
await importFixture(page,fixture({dates:['2025-01-01',date]}));
await page.getByRole('button',{name:'Réglages',exact:true}).click();
await expect(page.getByRole('button',{name:'Exporter les données',exact:true})).toHaveCount(0);
await expect(page.getByLabel('Conserver (jours)')).toHaveCount(0);
await page.getByRole('button',{name:'Gérer les données et sauvegardes'}).click();
await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible();
await expect(page.locator('nav').getByRole('button',{name:'Réglages',exact:true})).toHaveAttribute('aria-current','true');
const retention = page.getByLabel('Conserver (jours)');
await expect(retention).toHaveValue('30');
await retention.fill('60');
await retention.press('Tab');
await expect.poll(() => page.evaluate(() => new Promise((resolve,reject) => {
const request = indexedDB.open('TimeLogDB');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const query = db.transaction('settings').objectStore('settings').get('app');
query.onsuccess = () => { resolve(query.result.value.retentionDays); db.close(); };
query.onerror = () => { reject(query.error); db.close(); };
};
}))).toBe(60);
await page.reload();
await expect(retention).toHaveValue('60');
expect(await entries(page)).toHaveLength(6);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await page.getByRole('button',{name:'Retour aux réglages'}).click();
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
await page.getByRole('button',{name:'Gérer les données et sauvegardes'}).click();
await page.goBack();
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
await page.getByRole('button',{name:'Récap',exact:true}).click();
await page.getByRole('button',{name:'Sauvegarder',exact:true}).click();
await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible();
});
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);
await page.getByRole('button',{name:'Reprendre la journée',exact:true}).click();
let dialog = page.getByRole('dialog',{name:'Reprendre la journée',exact:true});
await expect(dialog.getByRole('radio',{name:'Créer une pause jusqu’à maintenant'})).toBeChecked();
await dialog.getByRole('button',{name:'Annuler'}).click();
expect(await entries(page)).toEqual(before);
await expect(page.getByRole('button',{name:'Reprendre la journée',exact:true})).toBeFocused();
const other = await context.newPage();
await other.clock.install({time:now}); await other.goto('/#/today');
await Promise.all([page.getByRole('button',{name:'Reprendre la journée',exact:true}).click(),other.getByRole('button',{name:'Reprendre la journée',exact:true}).click()]);
await Promise.all([page.getByRole('button',{name:'Confirmer la reprise'}).dblclick(),other.getByRole('button',{name:'Confirmer la reprise'}).click()]);
// Either tab may win the transaction; the resumed entries must remain unique.
await expect.poll(async () =>
await page.getByRole('heading',{name:'En cours',exact:true}).isVisible() ||
await other.getByRole('heading',{name:'En cours',exact:true}).isVisible()
).toBe(true);
const rows = await entries(page);
expect(rows.map(row=>row.kind)).toEqual(['work','break','work']);
expect(rows[0].endTs).toBe(at('17:00')); expect(rows[1].startTs).toBe(at('17:00'));
expect(Math.abs(rows[1].endTs-now)).toBeLessThan(60_000);
});
test('choix continu et reprise immédiate d’une pause sur Journée', async ({page}) => {
await importFixture(page,fixture({pause:false}));
await page.getByRole('button',{name:'Journée',exact:true}).click();
await page.getByRole('button',{name:'Reprendre la journée',exact:true}).click();
await page.getByRole('radio',{name:'Continuer depuis la fin enregistrée'}).check();
await page.getByRole('button',{name:'Confirmer la reprise'}).click();
await expect(page.getByRole('heading',{name:'En cours',exact:true})).toBeVisible();
expect(await entries(page)).toHaveLength(1);
await page.getByRole('button',{name:'Commencer une pause'}).click();
await expect(page.getByRole('heading',{name:'Pause en cours'})).toBeVisible();
await page.getByRole('button',{name:'Reprendre le travail'}).click();
const rows = await entries(page);
expect(rows.map(row=>row.kind)).toEqual(['work','break','work']);
expect(rows.at(-1)).toMatchObject({task:'Tâche de test',client:'Client de test',endTs:null});
});
for (const kind of ['lunch','break']) test(`reprise après ${kind} garde sa nature`, async ({page}) => {
await importFixture(page,fixture({pause:false,lastKind:kind}));
await page.getByRole('button',{name:'Reprendre la journée',exact:true}).click();
await page.getByRole('radio',{name:kind === 'lunch' ? 'Prolonger le repas depuis la fin enregistrée' : 'Prolonger la pause depuis la fin enregistrée'}).check();
await page.getByRole('button',{name:'Confirmer la reprise'}).click();
await expect(page.getByRole('heading',{name:'En cours',exact:true})).toBeVisible();
const rows = await entries(page); expect(rows[1].kind).toBe(kind); expect(rows.at(-1).kind).toBe('work');
});
test('export relu et vérifié, PWA relancée hors ligne', async ({page,context}) => {
await importFixture(page);
await page.getByRole('button',{name:'Réglages',exact:true}).click();
await page.getByRole('button',{name:'Gérer les données et sauvegardes'}).click();
const downloading = page.waitForEvent('download');
await page.getByRole('button',{name:'Exporter les données',exact:true}).click();
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 page.evaluate(async () => { await navigator.serviceWorker.ready; });
await page.reload();
await expect.poll(() => page.evaluate(() => Boolean(navigator.serviceWorker.controller))).toBe(true);
await context.setOffline(true);
await page.reload();
await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible();
await page.getByRole('button',{name:'Récap',exact:true}).click();
await expect(page.locator('.total')).toContainText('8h30');
});
test('échec IndexedDB visible et reprise avec Réessayer', async ({page}) => {
await page.addInitScript(() => {
const original = indexedDB.open.bind(indexedDB);
indexedDB.open = () => { throw new Error('IndexedDB indisponible pour le test'); };
window.enableTestStorage = () => (indexedDB.open = original);
});
await page.goto('/#/settings');
await expect(page.getByRole('alert')).toContainText('Accès aux données impossible');
await page.evaluate(() => window.enableTestStorage());
await page.getByRole('button',{name:'Réessayer',exact:true}).click();
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
});
test('purge confirmée et restauration de sa sauvegarde récupérable', async ({page}) => {
await importFixture(page,fixture({dates:['2025-01-01',date]}));
await page.getByRole('button',{name:'Réglages',exact:true}).click();
await page.getByRole('button',{name:'Gérer les données et sauvegardes'}).click();
await page.getByRole('button',{name:'Purger maintenant'}).click();
const dialog = page.getByRole('dialog',{name:'Confirmer la purge'});
await expect(dialog).toContainText('2025-01-01');
await dialog.getByRole('button',{name:'Annuler'}).click();
expect(await entries(page)).toHaveLength(6);
await page.getByRole('button',{name:'Purger maintenant'}).click();
await dialog.getByRole('button',{name:'Confirmer la purge'}).click();
await expect(dialog).toHaveCount(0);
expect(await entries(page)).toHaveLength(3);
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();
expect(await entries(page)).toHaveLength(6);
});
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();
await page.getByRole('button',{name:'Corriger cette journée'}).click();
await page.locator('.timeline button').nth(1).click();
const correction = page.getByRole('dialog',{name:'Modifier le poste'});
await correction.getByRole('button',{name:'Début du poste : 12:00'}).click();
const clock = page.getByRole('dialog',{name:'Début du poste',exact:true});
await expect(clock.getByRole('button',{name:'Fermer',exact:true})).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(clock.getByRole('button',{name:'Valider',exact:true})).toBeFocused();
await page.keyboard.press('Escape');
await expect(clock).toHaveCount(0);
await expect(correction.getByRole('button',{name:'Début du poste : 12:00'})).toBeFocused();
await page.keyboard.press('Escape');
await expect(correction).toHaveCount(0);
await expect(page.locator('.timeline button').nth(1)).toBeFocused();
});
test('pause ajoutée dans un intervalle vide sans combler ses portions restantes', async ({page}) => {
const payload = fixture({pause:false});
payload.entries = [
{...payload.entries[0],endTs:at('11:00')},
{...payload.entries[0],seq:1,startTs:at('13:00')},
];
await importFixture(page,payload);
await page.getByRole('button',{name:'Voir les détails'}).click();
await page.getByRole('button',{name:'Corriger cette journée'}).click();
await page.getByRole('button',{name:'Ajouter un poste oublié'}).click();
const dialog = page.getByRole('dialog',{name:'Ajouter un poste',exact:true});
await dialog.getByLabel('Plage à corriger').selectOption({label:'11:00–13:00 · Intervalle sans poste'});
await dialog.getByRole('button',{name:'Pause',exact:true}).click();
await dialog.getByRole('button',{name:'Début du poste : 11:00'}).click();
let clock = page.getByRole('dialog',{name:'Début du poste',exact:true});
await clock.getByLabel('Heures',{exact:true}).selectOption('12');
await clock.getByRole('button',{name:'Valider'}).click();
await dialog.getByRole('button',{name:'Fin du poste : 13:00'}).click();
clock = page.getByRole('dialog',{name:'Fin du poste',exact:true});
await clock.getByLabel('Heures',{exact:true}).selectOption('12');
await clock.getByLabel('Minutes',{exact:true}).selectOption('30');
await clock.getByRole('button',{name:'Valider'}).click();
await dialog.getByRole('button',{name:'Enregistrer',exact:true}).click();
await expect(dialog).toHaveCount(0);
const rows = await entries(page);
expect(rows.map(row=>row.kind)).toEqual(['work','break','work']);
expect(rows[0].endTs).toBe(at('11:00')); expect(rows[2].startTs).toBe(at('13:00'));
expect(rows[1]).toMatchObject({startTs:at('12:00'),endTs:at('12:30'),client:null});
});
test('réglage horaire restauré dans le champ après échec de sauvegarde', async ({page}) => {
await importFixture(page);
await page.getByRole('button',{name:'Réglages',exact:true}).click();
const input = page.getByLabel('Début de journée',{exact:true});
const original = await input.inputValue();
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 d’écriture simulé');
}
return originalPut.call(this,record,...args);
};
});
await input.fill('09:00'); await input.press('Tab');
await expect(page.locator('.error.card')).toContainText('Enregistrement impossible');
await expect(input).toHaveValue(original);
});