Multiple minor changes and improvements
This commit is contained in:
115
src/lib/break.test.js
Normal file
115
src/lib/break.test.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'fake-indexeddb/auto';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { db, getDay, getEntries, reopenDay, closeDay } from './db.js';
|
||||
import { buildResume, resumeToText } from './resume.js';
|
||||
import { buildStats } from './stats.js';
|
||||
import { buildBackup, parseBackup } from './backup.js';
|
||||
import { buildModel } from './predict.js';
|
||||
import { automateDay } from './automation.js';
|
||||
import { insertRecordedEntry, removeRecordedEntry, saveRecordedEntry } from './editing.js';
|
||||
import { DEFAULT_SETTINGS } from './settings.svelte.js';
|
||||
import { atLocalTime, HOUR, MINUTE } from './time.js';
|
||||
const date = '2026-10-08';
|
||||
const at = time => atLocalTime(date, time);
|
||||
const day = { date, status: 'closed', startedTs: at('08:00'), endedTs: at('17:00'), plannedStartTs: at('08:00'), plannedEndTs: at('17:00'), lunchType: 'panier' };
|
||||
const slot = (seq, kind, start, end) => ({ date, seq, kind, task: kind === 'break' ? 'Pause' : kind === 'lunch' ? 'Repas' : 'Chantier', client: kind === 'work' ? 'Client A' : null, startTs: at(start), endTs: end ? at(end) : null });
|
||||
beforeEach(async () => { await db.days.clear(); await db.entries.clear(); await db.settings.clear(); });
|
||||
async function install(entries, record = day) { await db.days.add(record); await db.entries.bulkAdd(entries); }
|
||||
|
||||
describe('pauses and confirmed reopening', () => {
|
||||
it('counts 8h30 work and 30 minutes break, with no recap or suggestion pollution', () => {
|
||||
const entries = [slot(0, 'work', '08:00','12:00'), slot(1,'break','12:00','12:30'),slot(2,'work','12:30','17:00')];
|
||||
const resume = buildResume(day, entries);
|
||||
expect(resume.workedMs).toBe(8.5 * HOUR);
|
||||
expect(resumeToText(resume)).not.toContain('Pause');
|
||||
const stats = buildStats([day], entries);
|
||||
expect(stats.breakMs).toBe(30 * MINUTE); expect(stats.lunchMs).toBe(0);
|
||||
expect(stats.tasks.map(t => t.name)).toEqual(['Chantier']);
|
||||
expect(buildModel(entries,[day]).taskCount.has('Pause')).toBe(false);
|
||||
});
|
||||
it('continues work from the recorded end', async () => {
|
||||
await install([slot(0,'work','08:00','17:00')]);
|
||||
expect(await reopenDay(date, at('18:00'), 'continue')).toBe(true);
|
||||
expect((await getEntries(date))).toHaveLength(1);
|
||||
expect(buildResume(await getDay(date),await getEntries(date), 15).workedMs).toBeGreaterThanOrEqual(9 * HOUR);
|
||||
await closeDay(date, at('18:00'));
|
||||
expect(buildResume(await getDay(date),await getEntries(date)).workedMs).toBe(10 * HOUR);
|
||||
});
|
||||
it('commits one discontinuous reopening under concurrent calls', async () => {
|
||||
await install([slot(0,'work','08:00','17:00')]);
|
||||
expect(await Promise.all([reopenDay(date,at('18:00'),'break'),reopenDay(date,at('18:00'),'break')])).toEqual([true,false]);
|
||||
const rows = await getEntries(date);
|
||||
expect(rows.map(e => e.kind)).toEqual(['work','break','work']);
|
||||
expect(rows[0].endTs).toBe(at('17:00')); expect(rows[1]).toMatchObject({startTs:at('17:00'),endTs:at('18:00')});
|
||||
expect(rows[2]).toMatchObject({startTs:at('18:00'),endTs:null,task:'Chantier',client:'Client A'});
|
||||
});
|
||||
for (const kind of ['lunch','break']) for (const mode of ['continue','break']) {
|
||||
it(`preserves a last ${kind} on ${mode}`, async () => {
|
||||
await install([slot(0,'work','08:00','16:00'),slot(1,kind,'16:00','17:00')]);
|
||||
await reopenDay(date,at('18:00'),mode);
|
||||
const rows = await getEntries(date);
|
||||
expect(rows[1].kind).toBe(kind);
|
||||
expect(rows[1].endTs).toBe(at(mode === 'continue' ? '18:00' : '17:00'));
|
||||
expect(rows.at(-1)).toMatchObject({kind:'work',startTs:at('18:00'),task:'Chantier'});
|
||||
});
|
||||
}
|
||||
it('creates no zero-duration break and rejects past days and invalid choices', async () => {
|
||||
await install([slot(0,'work','08:00','17:00')]);
|
||||
expect(await reopenDay(date,atLocalTime('2026-10-09','18:00'),'break')).toBe(false);
|
||||
await expect(reopenDay(date,at('18:00'),'invalid')).rejects.toThrow();
|
||||
expect((await getDay(date)).status).toBe('closed');
|
||||
await reopenDay(date,at('17:00'),'break');
|
||||
expect((await getEntries(date)).map(e=>e.kind)).toEqual(['work','work']);
|
||||
});
|
||||
it('rolls back the day and slots when writing the resumed work fails', async () => {
|
||||
await install([slot(0,'work','08:00','17:00')]);
|
||||
const real = db.entries.add.bind(db.entries);
|
||||
const spy = vi.spyOn(db.entries,'add').mockImplementation(record => record.kind === 'work' ? Promise.reject(new Error('write failure')) : real(record));
|
||||
try { await expect(reopenDay(date,at('18:00'),'break')).rejects.toThrow('write failure'); }
|
||||
finally { spy.mockRestore(); }
|
||||
expect((await getDay(date)).status).toBe('closed'); expect(await getEntries(date)).toHaveLength(1);
|
||||
});
|
||||
it('does not replace a pause at automatic lunch or closing', async () => {
|
||||
await install([slot(0,'work','08:00','11:30'),slot(1,'break','11:30',null)], {...day,status:'active',endedTs:null});
|
||||
await automateDay(date, {...DEFAULT_SETTINGS,autoLunch:true}, at('13:30'));
|
||||
expect((await getEntries(date)).at(-1).kind).toBe('break');
|
||||
await automateDay(date,DEFAULT_SETTINGS,at('17:00'),{end:true});
|
||||
expect((await getEntries(date)).at(-1)).toMatchObject({kind:'break',endTs:at('17:00')});
|
||||
});
|
||||
it('inserts, corrects and deletes a closed-day pause with recalculated totals', async () => {
|
||||
await install([slot(0,'work','08:00','17:00')]);
|
||||
const id = await insertRecordedEntry((await getEntries(date))[0], {...slot(0,'break','12:00','12:30')}, at('18:00'));
|
||||
let entry = await db.entries.get(id);
|
||||
await saveRecordedEntry(entry, {...entry,endTs:at('13:00')}, at('18:00'));
|
||||
expect(buildStats([await getDay(date)],await getEntries(date)).breakMs).toBe(HOUR);
|
||||
await removeRecordedEntry(await db.entries.get(id));
|
||||
expect(buildResume(await getDay(date),await getEntries(date)).workedMs).toBe(9*HOUR);
|
||||
expect((await getDay(date)).status).toBe('closed');
|
||||
});
|
||||
it('round-trips breaks and imported gaps, and imports v1', () => {
|
||||
const entries = [slot(0,'work','08:00','11:00'),slot(1,'break','12:00','12:30'),slot(2,'work','13:00','17:00')];
|
||||
const payload = buildBackup({days:[day],entries,settings:null});
|
||||
expect(payload.version).toBe(2);
|
||||
expect(parseBackup(JSON.stringify(payload)).entries).toEqual(entries);
|
||||
expect(parseBackup(JSON.stringify({...payload,version:1,entries:[slot(0,'work','08:00','17:00')]})).dayCount).toBe(1);
|
||||
expect(() => parseBackup(JSON.stringify({...payload,version:3}))).toThrow(/plus récente/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preserving imported gaps while editing', () => {
|
||||
it('does not fill a gap when correcting a pause', async () => {
|
||||
await install([slot(0,'work','08:00','11:00'),slot(1,'break','12:00','12:30'),slot(2,'work','13:00','17:00')]);
|
||||
const original = (await getEntries(date))[1];
|
||||
await saveRecordedEntry(original,{...original,startTs:at('11:30'),endTs:at('12:45')},at('18:00'));
|
||||
const rows = await getEntries(date);
|
||||
expect(rows[0].endTs).toBe(at('11:00')); expect(rows[2].startTs).toBe(at('13:00'));
|
||||
expect(buildStats([day],rows).quality.uncoveredMs).toBe(45*MINUTE);
|
||||
await expect(saveRecordedEntry(rows[1],{...rows[1],startTs:at('10:00')},at('18:00'))).rejects.toThrow(/voisin/);
|
||||
});
|
||||
it('does not fill surrounding gaps when deleting a pause', async () => {
|
||||
await install([slot(0,'work','08:00','11:00'),slot(1,'break','12:00','12:30'),slot(2,'work','13:00','17:00')]);
|
||||
await removeRecordedEntry((await getEntries(date))[1]);
|
||||
const rows = await getEntries(date);
|
||||
expect(rows[0].endTs).toBe(at('11:00')); expect(rows[1].startTs).toBe(at('13:00'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user