Nested UI for data & storage options
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
import Automations from './routes/Automations.svelte';
|
import Automations from './routes/Automations.svelte';
|
||||||
import NonWorkingSettings from './routes/NonWorkingSettings.svelte';
|
import NonWorkingSettings from './routes/NonWorkingSettings.svelte';
|
||||||
import Stats from './routes/Stats.svelte';
|
import Stats from './routes/Stats.svelte';
|
||||||
|
import DataSettings from './routes/DataSettings.svelte';
|
||||||
|
|
||||||
let ready = $state(false);
|
let ready = $state(false);
|
||||||
let bootError = $state('');
|
let bootError = $state('');
|
||||||
@@ -58,8 +59,8 @@
|
|||||||
{#if !ready}
|
{#if !ready}
|
||||||
<div class="boot">{#if bootError}<div class="stack"><p role="alert">{bootError}</p><button onclick={() => (retry += 1)}>Réessayer</button></div>{:else}<p>Chargement…</p>{/if}</div>
|
<div class="boot">{#if bootError}<div class="stack"><p role="alert">{bootError}</p><button onclick={() => (retry += 1)}>Réessayer</button></div>{:else}<p>Chargement…</p>{/if}</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if backupState.loaded && backupReminderDue(backupState.status, clockNow) && view !== 'settings'}
|
{#if backupState.loaded && backupReminderDue(backupState.status, clockNow) && !['settings', 'data'].includes(view)}
|
||||||
<div class="update"><span>Sauvegarde à vérifier</span><button disabled={dayState.importing} onclick={() => go('settings')}>Sauvegarder</button></div>
|
<div class="update"><span>Sauvegarde à vérifier</span><button disabled={dayState.importing} onclick={() => go('data')}>Sauvegarder</button></div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if needsRefresh.value && !dayState.day}
|
{#if needsRefresh.value && !dayState.day}
|
||||||
<!-- Only offered while no day is running, so an update can never interrupt
|
<!-- Only offered while no day is running, so an update can never interrupt
|
||||||
@@ -77,6 +78,8 @@
|
|||||||
<Automations />
|
<Automations />
|
||||||
{:else if view === 'non-working'}
|
{:else if view === 'non-working'}
|
||||||
<NonWorkingSettings />
|
<NonWorkingSettings />
|
||||||
|
{:else if view === 'data'}
|
||||||
|
<DataSettings />
|
||||||
{:else if ['settings', 'automations', 'non-working'].includes(view)}
|
{:else if ['settings', 'automations', 'non-working'].includes(view)}
|
||||||
<Settings />
|
<Settings />
|
||||||
{:else if view === 'today'}
|
{:else if view === 'today'}
|
||||||
@@ -104,9 +107,9 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class:sel={['settings', 'automations', 'non-working'].includes(view)}
|
class:sel={['settings', 'automations', 'non-working', 'data'].includes(view)}
|
||||||
disabled={dayState.importing}
|
disabled={dayState.importing}
|
||||||
aria-current={['settings', 'automations', 'non-working'].includes(view)}
|
aria-current={['settings', 'automations', 'non-working', 'data'].includes(view)}
|
||||||
onclick={() => go('settings')}>Réglages</button
|
onclick={() => go('settings')}>Réglages</button
|
||||||
>
|
>
|
||||||
{#if view === 'stats'}
|
{#if view === 'stats'}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="card stack" aria-busy={busy}>
|
<section class="card stack" aria-busy={busy}>
|
||||||
<h2>Données</h2>
|
<h2>Sauvegarde et restauration</h2>
|
||||||
|
|
||||||
<button type="button" disabled={busy} onclick={exportAll}>Exporter les données</button>
|
<button type="button" disabled={busy} onclick={exportAll}>Exporter les données</button>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* worker should land on.
|
* worker should land on.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ROUTES = ['resume', 'today', 'settings', 'stats', 'details', 'automations', 'non-working'];
|
const ROUTES = ['resume', 'today', 'settings', 'stats', 'details', 'automations', 'non-working', 'data'];
|
||||||
|
|
||||||
function fromHash() {
|
function fromHash() {
|
||||||
const [path, query = ''] = location.hash.replace(/^#\/?/, '').split('?');
|
const [path, query = ''] = location.hash.replace(/^#\/?/, '').split('?');
|
||||||
|
|||||||
121
src/routes/DataSettings.svelte
Normal file
121
src/routes/DataSettings.svelte
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<script>
|
||||||
|
import BackupPanel from '../components/BackupPanel.svelte';
|
||||||
|
import Sheet from '../components/Sheet.svelte';
|
||||||
|
import { go } from '../lib/router.svelte.js';
|
||||||
|
import { purgeCandidates, purgeReviewedDays } from '../lib/backup-safety.js';
|
||||||
|
import { dayState, refresh } from '../lib/day.svelte.js';
|
||||||
|
import { settings, settingsError, updateSettings } from '../lib/settings.svelte.js';
|
||||||
|
|
||||||
|
let purged = $state(null);
|
||||||
|
let purgeOpen = $state(false);
|
||||||
|
let purgeError = $state('');
|
||||||
|
let purgeBusy = $state(false);
|
||||||
|
let reviewedPurge = $state([]);
|
||||||
|
let reviewedRetention = $state(0);
|
||||||
|
function reviewPurge() {
|
||||||
|
reviewedRetention = settings.retentionDays;
|
||||||
|
reviewedPurge = purgeCandidates(dayState.closedDays, reviewedRetention).map(day => ({
|
||||||
|
day: { ...day }, entries: dayState.historyEntries.filter(entry => entry.date === day.date).sort((a, b) => a.seq - b.seq).map(entry => ({ ...entry })),
|
||||||
|
}));
|
||||||
|
purgeError = ''; purgeOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRetention(event) {
|
||||||
|
const input = event.currentTarget;
|
||||||
|
await updateSettings({ retentionDays: Number(input.value) });
|
||||||
|
input.value = String(settings.retentionDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function purge() {
|
||||||
|
if (purgeBusy || dayState.importing) return;
|
||||||
|
purgeBusy = true; purgeError = '';
|
||||||
|
try {
|
||||||
|
purged = await purgeReviewedDays($state.snapshot(reviewedPurge), reviewedRetention);
|
||||||
|
await refresh(); purgeOpen = false;
|
||||||
|
} catch (error) { purgeError = error.message; }
|
||||||
|
finally { purgeBusy = false; }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<fieldset class="screen" disabled={dayState.importing}>
|
||||||
|
<h1>Données et sauvegardes</h1>
|
||||||
|
<button type="button" onclick={() => go('settings')}>Retour aux réglages</button>
|
||||||
|
|
||||||
|
<section class="card stack">
|
||||||
|
<h2>Vos données sur cet appareil</h2>
|
||||||
|
<p class="small">Vos journées, postes et réglages sont enregistrés dans ce navigateur, sur cet appareil, et restent accessibles hors ligne. Ils ne sont pas synchronisés entre vos appareils.</p>
|
||||||
|
<p class="small">Effacer les données du navigateur ou perdre cet appareil peut vous faire perdre votre historique. Exportez régulièrement un fichier, vérifiez-le puis gardez une copie ailleurs : vous pourrez le réimporter pour retrouver vos données ou les transférer sur un autre appareil.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if settingsError.message}
|
||||||
|
<p class="error card" role="alert">{settingsError.message}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<BackupPanel />
|
||||||
|
|
||||||
|
<section class="card stack">
|
||||||
|
<h2>Durée de conservation</h2>
|
||||||
|
<label>
|
||||||
|
<span>Conserver (jours)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="30"
|
||||||
|
step="30"
|
||||||
|
value={settings.retentionDays}
|
||||||
|
onchange={saveRetention}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p class="dim small">
|
||||||
|
Cette durée détermine les journées anciennes proposées à la purge. La suppression
|
||||||
|
se fait uniquement avec « Purger maintenant », après confirmation ; changer la durée
|
||||||
|
ne supprime rien. La dernière journée terminée est toujours conservée.
|
||||||
|
</p>
|
||||||
|
<p class="dim small">
|
||||||
|
L’historique alimente les suggestions et les statistiques. La purge conserve une
|
||||||
|
sauvegarde récupérable sur cet appareil, remplacée à la prochaine purge. Exportez
|
||||||
|
aussi un fichier pour garder une copie ailleurs.
|
||||||
|
</p>
|
||||||
|
<button type="button" onclick={reviewPurge}>Purger maintenant</button>
|
||||||
|
{#if purged !== null}
|
||||||
|
<p class="dim small">
|
||||||
|
{purged === 0
|
||||||
|
? 'Aucune journée plus ancienne que la durée de conservation.'
|
||||||
|
: `${purged} journée${purged > 1 ? 's' : ''} supprimée${purged > 1 ? 's' : ''}.`}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<Sheet open={purgeOpen} title="Confirmer la purge" onclose={() => { if (!purgeBusy) purgeOpen = false; }}>
|
||||||
|
<p>Journées concernées : {reviewedPurge.length ? reviewedPurge.map(item => item.day.date).join(', ') : 'aucune'}.</p>
|
||||||
|
<p>Une sauvegarde récupérable sera conservée sur cet appareil avant la suppression.</p>
|
||||||
|
{#if purgeError}<p role="alert">{purgeError}</p>{/if}
|
||||||
|
<button disabled={purgeBusy || dayState.importing || !reviewedPurge.length} class="btn-danger" onclick={purge}>Confirmer la purge</button>
|
||||||
|
<button disabled={purgeBusy} onclick={() => (purgeOpen = false)}>Annuler</button>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
fieldset.screen {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--danger-fg);
|
||||||
|
background: var(--danger);
|
||||||
|
border-color: var(--danger);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import BackupPanel from '../components/BackupPanel.svelte';
|
|
||||||
import { go } from '../lib/router.svelte.js';
|
import { go } from '../lib/router.svelte.js';
|
||||||
import Sheet from '../components/Sheet.svelte';
|
import { dayState } from '../lib/day.svelte.js';
|
||||||
import { purgeCandidates, purgeReviewedDays } from '../lib/backup-safety.js';
|
|
||||||
import { dayState, refresh } from '../lib/day.svelte.js';
|
|
||||||
import {
|
import {
|
||||||
ROUNDING_CHOICES,
|
ROUNDING_CHOICES,
|
||||||
settings,
|
settings,
|
||||||
@@ -13,20 +10,6 @@
|
|||||||
WEEKDAY_LABELS,
|
WEEKDAY_LABELS,
|
||||||
} from '../lib/settings.svelte.js';
|
} from '../lib/settings.svelte.js';
|
||||||
|
|
||||||
let purged = $state(null);
|
|
||||||
let purgeOpen = $state(false);
|
|
||||||
let purgeError = $state('');
|
|
||||||
let purgeBusy = $state(false);
|
|
||||||
let reviewedPurge = $state([]);
|
|
||||||
let reviewedRetention = $state(0);
|
|
||||||
function reviewPurge() {
|
|
||||||
reviewedRetention = settings.retentionDays;
|
|
||||||
reviewedPurge = purgeCandidates(dayState.closedDays, reviewedRetention).map(day => ({
|
|
||||||
day: { ...day }, entries: dayState.historyEntries.filter(entry => entry.date === day.date).sort((a, b) => a.seq - b.seq).map(entry => ({ ...entry })),
|
|
||||||
}));
|
|
||||||
purgeError = ''; purgeOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleWorkday(value) {
|
function toggleWorkday(value) {
|
||||||
return setWorkingDay(value, !settings.workdays.includes(value));
|
return setWorkingDay(value, !settings.workdays.includes(value));
|
||||||
}
|
}
|
||||||
@@ -36,16 +19,6 @@
|
|||||||
await updateSettings({ [key]: numeric ? Number(input.value) : input.value });
|
await updateSettings({ [key]: numeric ? Number(input.value) : input.value });
|
||||||
input.value = String(settings[key]);
|
input.value = String(settings[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function purge() {
|
|
||||||
if (purgeBusy || dayState.importing) return;
|
|
||||||
purgeBusy = true; purgeError = '';
|
|
||||||
try {
|
|
||||||
purged = await purgeReviewedDays($state.snapshot(reviewedPurge), reviewedRetention);
|
|
||||||
await refresh(); purgeOpen = false;
|
|
||||||
} catch (error) { purgeError = error.message; }
|
|
||||||
finally { purgeBusy = false; }
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset class="screen" disabled={dayState.importing}>
|
<fieldset class="screen" disabled={dayState.importing}>
|
||||||
@@ -175,45 +148,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<BackupPanel />
|
|
||||||
|
|
||||||
<section class="card stack">
|
<section class="card stack">
|
||||||
<h2>Historique</h2>
|
<h2>Données et sauvegardes</h2>
|
||||||
<label>
|
<p class="dim small">Sauvegarder, exporter ou importer les données et régler leur durée de conservation.</p>
|
||||||
<span>Conserver (jours)</span>
|
<button type="button" onclick={() => go('data')}>Gérer les données et sauvegardes</button>
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="30"
|
|
||||||
step="30"
|
|
||||||
value={settings.retentionDays}
|
|
||||||
onchange={(e) => saveField(e, 'retentionDays', true)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<p class="dim small">
|
|
||||||
L'historique alimente les suggestions : plus il est court, moins les propositions
|
|
||||||
sont justes. La dernière journée close est toujours conservée.
|
|
||||||
</p>
|
|
||||||
<p class="dim small">
|
|
||||||
La purge nécessite une confirmation et conserve une sauvegarde locale récupérable.
|
|
||||||
</p>
|
|
||||||
<button type="button" onclick={reviewPurge}>Purger maintenant</button>
|
|
||||||
{#if purged !== null}
|
|
||||||
<p class="dim small">
|
|
||||||
{purged === 0
|
|
||||||
? 'Aucune journée plus ancienne que la durée de conservation.'
|
|
||||||
: `${purged} journée${purged > 1 ? 's' : ''} supprimée${purged > 1 ? 's' : ''}.`}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</section>
|
</section>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<Sheet open={purgeOpen} title="Confirmer la purge" onclose={() => { if (!purgeBusy) purgeOpen = false; }}>
|
|
||||||
<p>Journées concernées : {reviewedPurge.length ? reviewedPurge.map(item => item.day.date).join(', ') : 'aucune'}.</p>
|
|
||||||
<p>Une sauvegarde récupérable sera conservée sur cet appareil avant la suppression.</p>
|
|
||||||
{#if purgeError}<p role="alert">{purgeError}</p>{/if}
|
|
||||||
<button disabled={purgeBusy || dayState.importing || !reviewedPurge.length} class="btn-danger" onclick={purge}>Confirmer la purge</button>
|
|
||||||
<button disabled={purgeBusy} onclick={() => (purgeOpen = false)}>Annuler</button>
|
|
||||||
</Sheet>
|
|
||||||
<style>
|
<style>
|
||||||
fieldset.screen {
|
fieldset.screen {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ async function importFixture(page, payload = fixture()) {
|
|||||||
await page.clock.install({ time:now });
|
await page.clock.install({ time:now });
|
||||||
await page.goto('/#/settings');
|
await page.goto('/#/settings');
|
||||||
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
|
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.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('button',{name:'Remplacer tout',exact:true}).click();
|
||||||
await page.getByRole('dialog',{name:'Remplacer les données'}).getByRole('button',{name:'Confirmer',exact:true}).click();
|
await page.getByRole('dialog',{name:'Remplacer les données'}).getByRole('button',{name:'Confirmer',exact:true}).click();
|
||||||
@@ -88,6 +90,42 @@ test('date conservée, rechargement et Retour depuis les trois nouvelles pages',
|
|||||||
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
|
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}) => {
|
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);
|
||||||
@@ -101,7 +139,11 @@ test('reprise annulée puis concurrente entre deux onglets, choix pause par déf
|
|||||||
await other.clock.install({time:now}); await other.goto('/#/today');
|
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:'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 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();
|
// 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);
|
const rows = await entries(page);
|
||||||
expect(rows.map(row=>row.kind)).toEqual(['work','break','work']);
|
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(rows[0].endTs).toBe(at('17:00')); expect(rows[1].startTs).toBe(at('17:00'));
|
||||||
@@ -136,6 +178,7 @@ for (const kind of ['lunch','break']) test(`reprise après ${kind} garde sa natu
|
|||||||
test('export relu et vérifié, PWA relancée hors ligne', async ({page,context}) => {
|
test('export relu et vérifié, PWA relancée hors ligne', async ({page,context}) => {
|
||||||
await importFixture(page);
|
await importFixture(page);
|
||||||
await page.getByRole('button',{name:'Réglages',exact:true}).click();
|
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');
|
const downloading = page.waitForEvent('download');
|
||||||
await page.getByRole('button',{name:'Exporter les données',exact:true}).click();
|
await page.getByRole('button',{name:'Exporter les données',exact:true}).click();
|
||||||
const download = await downloading;
|
const download = await downloading;
|
||||||
@@ -147,7 +190,7 @@ test('export relu et vérifié, PWA relancée hors ligne', async ({page,context}
|
|||||||
await expect.poll(() => page.evaluate(() => Boolean(navigator.serviceWorker.controller))).toBe(true);
|
await expect.poll(() => page.evaluate(() => Boolean(navigator.serviceWorker.controller))).toBe(true);
|
||||||
await context.setOffline(true);
|
await context.setOffline(true);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByRole('heading',{name:'Réglages',exact:true})).toBeVisible();
|
await expect(page.getByRole('heading',{name:'Données et sauvegardes',exact:true})).toBeVisible();
|
||||||
await page.getByRole('button',{name:'Récap',exact:true}).click();
|
await page.getByRole('button',{name:'Récap',exact:true}).click();
|
||||||
await expect(page.locator('.total')).toContainText('8h30');
|
await expect(page.locator('.total')).toContainText('8h30');
|
||||||
});
|
});
|
||||||
@@ -168,6 +211,7 @@ test('échec IndexedDB visible et reprise avec Réessayer', async ({page}) => {
|
|||||||
test('purge confirmée et restauration de sa sauvegarde récupérable', async ({page}) => {
|
test('purge confirmée et restauration de sa sauvegarde récupérable', async ({page}) => {
|
||||||
await importFixture(page,fixture({dates:['2025-01-01',date]}));
|
await importFixture(page,fixture({dates:['2025-01-01',date]}));
|
||||||
await page.getByRole('button',{name:'Réglages',exact:true}).click();
|
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();
|
await page.getByRole('button',{name:'Purger maintenant'}).click();
|
||||||
const dialog = page.getByRole('dialog',{name:'Confirmer la purge'});
|
const dialog = page.getByRole('dialog',{name:'Confirmer la purge'});
|
||||||
await expect(dialog).toContainText('2025-01-01');
|
await expect(dialog).toContainText('2025-01-01');
|
||||||
|
|||||||
Reference in New Issue
Block a user