250 lines
15 KiB
JavaScript
250 lines
15 KiB
JavaScript
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.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);
|
||
});
|
||
|
||
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('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()]);
|
||
await expect(page.getByRole('heading',{name:'En cours',exact:true})).toBeVisible();
|
||
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();
|
||
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:'Réglages',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:'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);
|
||
});
|