mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
cleanup and deduplicate breaks frontend tests
This commit is contained in:
@@ -2,14 +2,31 @@ import { describe, expect, it } from 'vitest';
|
||||
import type { TimeEntry } from '@/packages/api/src';
|
||||
import {
|
||||
findMisplacedBreak,
|
||||
getBreakPlacementHint,
|
||||
type BreakPlacementHint,
|
||||
} from '@/packages/ui/src/utils/breakPlacement';
|
||||
|
||||
// Decision logic behind the aggregate (collapsed grouped-break) row's placement
|
||||
// warning: the row shows the hint — and navigates the calendar — based on the
|
||||
// first misplaced break in the group.
|
||||
function breakEntry(id: string): TimeEntry {
|
||||
return { id, type: 'break', start: '2026-07-14T10:00:00Z' } as TimeEntry;
|
||||
function entry(
|
||||
id: string,
|
||||
start: string,
|
||||
end: string | null,
|
||||
type: 'work' | 'break' = 'work'
|
||||
): TimeEntry {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
start,
|
||||
end,
|
||||
duration: end ? (Date.parse(end) - Date.parse(start)) / 1000 : null,
|
||||
organization_id: 'organization-1',
|
||||
user_id: 'user-1',
|
||||
member_id: 'member-1',
|
||||
project_id: type === 'break' ? null : 'project-1',
|
||||
task_id: null,
|
||||
billable: false,
|
||||
description: null,
|
||||
tags: [],
|
||||
};
|
||||
}
|
||||
|
||||
function hint(misplaced: boolean): BreakPlacementHint {
|
||||
@@ -24,34 +41,99 @@ function hint(misplaced: boolean): BreakPlacementHint {
|
||||
|
||||
describe('findMisplacedBreak', () => {
|
||||
it('returns the first misplaced break in a group', () => {
|
||||
const entries = [breakEntry('break-a'), breakEntry('break-b')];
|
||||
const entries = [
|
||||
entry('break-a', '2026-07-14T10:00:00Z', '2026-07-14T10:30:00Z', 'break'),
|
||||
entry('break-b', '2026-07-14T12:00:00Z', '2026-07-14T12:30:00Z', 'break'),
|
||||
];
|
||||
const result = findMisplacedBreak(entries, {
|
||||
'break-a': hint(false),
|
||||
'break-b': hint(true),
|
||||
});
|
||||
expect(result?.id).toBe('break-b');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when no break in the group is misplaced', () => {
|
||||
const entries = [breakEntry('break-a'), breakEntry('break-b')];
|
||||
const result = findMisplacedBreak(entries, {
|
||||
'break-a': hint(false),
|
||||
'break-b': hint(false),
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
describe('getBreakPlacementHint', () => {
|
||||
const breakEntry = entry('break', '2026-07-14T12:00:00Z', '2026-07-14T12:30:00Z', 'break');
|
||||
|
||||
it('accepts work touching both sides of the break', () => {
|
||||
const result = getBreakPlacementHint(breakEntry, [
|
||||
entry('morning', '2026-07-14T09:00:00Z', '2026-07-14T12:00:00Z'),
|
||||
entry('afternoon', '2026-07-14T12:30:00Z', '2026-07-14T17:00:00Z'),
|
||||
]);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
misplaced: false,
|
||||
gapBeforeSeconds: 0,
|
||||
gapAfterSeconds: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null when the group has no placement hints', () => {
|
||||
const entries = [breakEntry('break-a'), breakEntry('break-b')];
|
||||
expect(findMisplacedBreak(entries, {})).toBeNull();
|
||||
it('accepts gaps exactly at the placement tolerance', () => {
|
||||
const result = getBreakPlacementHint(breakEntry, [
|
||||
entry('morning', '2026-07-14T09:00:00Z', '2026-07-14T11:30:00Z'),
|
||||
entry('afternoon', '2026-07-14T13:00:00Z', '2026-07-14T17:00:00Z'),
|
||||
]);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
misplaced: false,
|
||||
gapBeforeSeconds: 30 * 60,
|
||||
gapAfterSeconds: 30 * 60,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores hints for entries that are not in the group', () => {
|
||||
const entries = [breakEntry('break-a')];
|
||||
const result = findMisplacedBreak(entries, {
|
||||
'break-a': hint(false),
|
||||
'break-elsewhere': hint(true),
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
it('flags a completed break when work is missing on either side', () => {
|
||||
const noPreviousWork = getBreakPlacementHint(breakEntry, [
|
||||
entry('afternoon', '2026-07-14T12:30:00Z', '2026-07-14T17:00:00Z'),
|
||||
]);
|
||||
const noNextWork = getBreakPlacementHint(breakEntry, [
|
||||
entry('morning', '2026-07-14T09:00:00Z', '2026-07-14T12:00:00Z'),
|
||||
]);
|
||||
|
||||
expect(noPreviousWork).toEqual(
|
||||
expect.objectContaining({ misplaced: true, gapBeforeSeconds: null })
|
||||
);
|
||||
expect(noNextWork).toEqual(
|
||||
expect.objectContaining({ misplaced: true, gapAfterSeconds: null })
|
||||
);
|
||||
});
|
||||
|
||||
it('does not require work after a running break', () => {
|
||||
const runningBreak = entry('break', '2026-07-14T12:00:00Z', null, 'break');
|
||||
const result = getBreakPlacementHint(runningBreak, [
|
||||
entry('morning', '2026-07-14T09:00:00Z', '2026-07-14T12:00:00Z'),
|
||||
]);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
misplaced: false,
|
||||
gapBeforeSeconds: 0,
|
||||
gapAfterSeconds: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('treats work overlapping the break as touching both sides', () => {
|
||||
const result = getBreakPlacementHint(breakEntry, [
|
||||
entry('overlapping', '2026-07-14T11:45:00Z', '2026-07-14T12:15:00Z'),
|
||||
]);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
misplaced: false,
|
||||
gapBeforeSeconds: 0,
|
||||
gapAfterSeconds: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for work entries', () => {
|
||||
expect(
|
||||
getBreakPlacementHint(entry('work', '2026-07-14T09:00:00Z', '2026-07-14T10:00:00Z'), [])
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('decideBreakPlacement', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('delegates an empty day to the default cell placement', () => {
|
||||
it('selects generic free-window placement for an empty day', () => {
|
||||
expect(
|
||||
decideBreakPlacement({
|
||||
date: DAY,
|
||||
@@ -88,6 +88,19 @@ describe('decideBreakPlacement', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('saves the break next to work too short to split', () => {
|
||||
const decision = decideBreakPlacement({
|
||||
date: DAY,
|
||||
durationSeconds: HALF_HOUR,
|
||||
context: context([movable('quick', 9, 9 + 1 / 60)]),
|
||||
});
|
||||
|
||||
expect(decision).toEqual({
|
||||
kind: 'save',
|
||||
slot: { start: `${DAY}T09:01:00Z`, end: `${DAY}T09:31:00Z` },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a day that cannot fit or rearrange the break', () => {
|
||||
expect(
|
||||
decideBreakPlacement({
|
||||
@@ -190,15 +203,6 @@ describe('planSplitEntry', () => {
|
||||
expect(workSeconds(plan!)).toBe(8 * HOUR);
|
||||
});
|
||||
|
||||
it('inserts a break as long as the work without shortening it', () => {
|
||||
const plan = planSplitEntry(iv(9, 10), HOUR);
|
||||
expect(plan).not.toBeNull();
|
||||
expect(plan!.firstHalf).toEqual({ start: `${DAY}T09:00:00Z`, end: `${DAY}T09:30:00Z` });
|
||||
expect(plan!.breakSlot).toEqual({ start: `${DAY}T09:30:00Z`, end: `${DAY}T10:30:00Z` });
|
||||
expect(plan!.secondHalf).toEqual({ start: `${DAY}T10:30:00Z`, end: `${DAY}T11:00:00Z` });
|
||||
expect(workSeconds(plan!)).toBe(HOUR);
|
||||
});
|
||||
|
||||
it('accepts a break far longer than the work entry', () => {
|
||||
const plan = planSplitEntry(iv(9, 9.5), 4 * HOUR);
|
||||
expect(plan).not.toBeNull();
|
||||
@@ -279,15 +283,6 @@ describe('planSplitEntry', () => {
|
||||
expect(workSeconds(plan!)).toBe(1.5 * HOUR);
|
||||
});
|
||||
|
||||
it('fits a break that is longer than the work left in the day', () => {
|
||||
const plan = planSplitEntry(iv(23, 24), HOUR, undefined, { dayStart, dayEnd });
|
||||
expect(plan).not.toBeNull();
|
||||
expect(plan!.firstHalf).toEqual({ start: `${DAY}T22:00:00Z`, end: `${DAY}T22:30:00Z` });
|
||||
expect(plan!.breakSlot).toEqual({ start: `${DAY}T22:30:00Z`, end: `${DAY}T23:30:00Z` });
|
||||
expect(plan!.secondHalf).toEqual({ start: `${DAY}T23:30:00Z`, end: MIDNIGHT });
|
||||
expect(workSeconds(plan!)).toBe(HOUR);
|
||||
});
|
||||
|
||||
it('reproduces its own plan when re-planned from the break start it chose', () => {
|
||||
const options = { dayStart, dayEnd };
|
||||
const suggested = planSplitEntry(iv(22, 23.5), HOUR, undefined, options);
|
||||
@@ -349,17 +344,6 @@ describe('findBreakSlotNearInDay', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('slides only as far as the nearest free window allows', () => {
|
||||
expect(findBreakSlotNearInDay(dayStart, dayEnd, HOUR, anchor, [iv(21.5, 22)])).toEqual({
|
||||
start: `${DAY}T23:00:00Z`,
|
||||
end: MIDNIGHT,
|
||||
});
|
||||
expect(findBreakSlotNearInDay(dayStart, dayEnd, 2 * HOUR, anchor, [iv(21.5, 22)])).toEqual({
|
||||
start: `${DAY}T22:00:00Z`,
|
||||
end: MIDNIGHT,
|
||||
});
|
||||
});
|
||||
|
||||
it('respects a day window shortened by a running entry', () => {
|
||||
expect(findBreakSlotNearInDay(dayStart, `${DAY}T12:00:00Z`, HOUR, anchor)).toEqual({
|
||||
start: `${DAY}T11:00:00Z`,
|
||||
@@ -396,10 +380,6 @@ describe('findAdjacentBreakSlot', () => {
|
||||
it('returns null when neither side fits inside the day', () => {
|
||||
expect(findAdjacentBreakSlot([iv(0, 24)], dayStart, dayEnd, HALF_HOUR)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null without any work to sit next to', () => {
|
||||
expect(findAdjacentBreakSlot([], dayStart, dayEnd, HALF_HOUR)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('planMoveInsert', () => {
|
||||
@@ -425,32 +405,6 @@ describe('planMoveInsert', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves an oversized gap alone instead of pulling the right block flush', () => {
|
||||
// 09-12 and 15-17 (3h gap). Break flush after first at 12:00 fits in the gap
|
||||
// → nothing moves; the user's gap is preserved.
|
||||
const plan = planMoveInsert(
|
||||
[movable('a', 9, 12), movable('b', 15, 17)],
|
||||
dayStart,
|
||||
dayEnd,
|
||||
`${DAY}T12:00:00Z`,
|
||||
HALF_HOUR
|
||||
);
|
||||
expect(plan!.breakSlot).toEqual({ start: `${DAY}T12:00:00Z`, end: `${DAY}T12:30:00Z` });
|
||||
expect(plan!.shifted).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not drag entries flush when the break sits mid-gap', () => {
|
||||
// Break at 13:00 in the middle of the 12:00-15:00 gap → neither side moves.
|
||||
const plan = planMoveInsert(
|
||||
[movable('a', 9, 12), movable('b', 15, 17)],
|
||||
dayStart,
|
||||
dayEnd,
|
||||
`${DAY}T13:00:00Z`,
|
||||
HALF_HOUR
|
||||
);
|
||||
expect(plan!.shifted).toEqual([]);
|
||||
});
|
||||
|
||||
it('shifts each side only as much as needed to clear the slot', () => {
|
||||
// Break 14:45-15:15 overlaps only the start of 'b' → 'b' pushed 15m later,
|
||||
// 'a' untouched.
|
||||
@@ -666,12 +620,6 @@ describe('findValidBreakGapNear', () => {
|
||||
expect(gap).toEqual({ start: `${DAY}T10:00:00Z`, end: `${DAY}T11:00:00Z` });
|
||||
});
|
||||
|
||||
it('clamps the anchor into the tolerance window when it sits too late', () => {
|
||||
// Anchored at 11:00 (beyond the window) → clamped back to 10:30.
|
||||
const gap = findValidBreakGapNear(work, HOUR, `${DAY}T11:00:00Z`);
|
||||
expect(gap).toEqual({ start: `${DAY}T10:30:00Z`, end: `${DAY}T11:30:00Z` });
|
||||
});
|
||||
|
||||
it('keeps the break in place inside an oversized gap', () => {
|
||||
// 09-10 and 14-15 → 4h gap. The break stays exactly where the user left it;
|
||||
// its distance from work is a soft hint, not a reason to move it.
|
||||
|
||||
@@ -155,49 +155,6 @@ describe('useBreakPlacement.placeBreak', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('parks the break next to work too short to split instead of refusing it', async () => {
|
||||
const work = entry('2026-04-10T09:00:00Z', '2026-04-10T09:01:00Z', { id: 'w1' });
|
||||
const { bp } = setup([work]);
|
||||
|
||||
await bp.placeBreak(breakRow, 0, HOUR / 2);
|
||||
|
||||
expect(apiMocks.createTimeEntry).toHaveBeenCalledTimes(1);
|
||||
expect(apiMocks.createTimeEntry.mock.calls[0]![0]).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'break',
|
||||
start: '2026-04-10T09:01:00Z',
|
||||
end: '2026-04-10T09:31:00Z',
|
||||
})
|
||||
);
|
||||
expect(bp.breakPlacementRequest.value).toBeNull();
|
||||
});
|
||||
|
||||
it('still offers a split when a running entry caps the end of the day', async () => {
|
||||
const work = entry('2026-04-10T09:00:00Z', '2026-04-10T12:00:00Z', { id: 'w1' });
|
||||
const running = entry('2026-04-10T12:00:00Z', null, { id: 'running' });
|
||||
const { bp } = setup([work, running]);
|
||||
|
||||
await expect(bp.placeBreak(breakRow, 0, HOUR)).resolves.toBe('needs-input');
|
||||
expect(bp.breakPlacementRequest.value?.defaultBreakStart).toBe('2026-04-10T09:30:00Z');
|
||||
|
||||
await bp.applyBreakPlacement('2026-04-10T09:30:00Z', HOUR);
|
||||
const created = apiMocks.createTimeEntry.mock.calls.map((c) => c[0]);
|
||||
expect(created).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'work',
|
||||
start: '2026-04-10T10:30:00Z',
|
||||
end: '2026-04-10T12:00:00Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'break',
|
||||
start: '2026-04-10T09:30:00Z',
|
||||
end: '2026-04-10T10:30:00Z',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('drops a new break into the first free window when the day has no work', async () => {
|
||||
const { bp, createCell } = setup([]);
|
||||
await bp.placeBreak(breakRow, 0, HOUR);
|
||||
@@ -223,41 +180,6 @@ describe('useBreakPlacement.placeBreak', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('slides a workless-day break earlier rather than growing it past midnight', async () => {
|
||||
const brk = entry('2026-04-10T23:00:00Z', '2026-04-10T23:30:00Z', {
|
||||
id: 'b1',
|
||||
type: 'break',
|
||||
});
|
||||
const { bp, updateEntry } = setup([brk]);
|
||||
|
||||
await bp.placeBreak(breakRow, 0, 2 * HOUR, 'b1');
|
||||
|
||||
expect(updateEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'b1',
|
||||
start: '2026-04-10T22:00:00Z',
|
||||
end: '2026-04-11T00:00:00Z',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses a workless-day resize the day genuinely cannot hold', async () => {
|
||||
const brk = entry('2026-04-10T23:00:00Z', '2026-04-10T23:30:00Z', {
|
||||
id: 'b1',
|
||||
type: 'break',
|
||||
});
|
||||
const other = entry('2026-04-10T06:00:00Z', '2026-04-10T18:00:00Z', {
|
||||
id: 'b2',
|
||||
type: 'break',
|
||||
});
|
||||
const { bp, updateEntry } = setup([brk, other]);
|
||||
|
||||
await expect(bp.placeBreak(breakRow, 0, 20 * HOUR, 'b1')).rejects.toBeInstanceOf(
|
||||
NoFreeWindowError
|
||||
);
|
||||
expect(updateEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('requests input in the split modal when a single work entry blocks every gap', async () => {
|
||||
const work = entry('2026-04-10T09:00:00Z', '2026-04-10T17:00:00Z', { id: 'w1' });
|
||||
const { bp } = setup([work]);
|
||||
@@ -320,63 +242,6 @@ describe('useBreakPlacement.applyBreakPlacement (split)', () => {
|
||||
expect(addNotification).toHaveBeenCalledWith('success', 'Break added', expect.any(String));
|
||||
});
|
||||
|
||||
it('keeps the work length when the break is as long as the work entry', async () => {
|
||||
const work = entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', { id: 'w1' });
|
||||
const { bp, updateEntry } = setup([work]);
|
||||
|
||||
await bp.placeBreak(breakRow, 0, HOUR);
|
||||
expect(bp.breakPlacementRequest.value?.defaultBreakStart).toBe('2026-04-10T09:30:00Z');
|
||||
await bp.applyBreakPlacement('2026-04-10T09:30:00Z', HOUR);
|
||||
|
||||
expect(updateEntry).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'w1',
|
||||
start: '2026-04-10T09:00:00Z',
|
||||
end: '2026-04-10T09:30:00Z',
|
||||
})
|
||||
);
|
||||
const created = apiMocks.createTimeEntry.mock.calls.map((c) => c[0]);
|
||||
expect(created).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'work',
|
||||
start: '2026-04-10T10:30:00Z',
|
||||
end: '2026-04-10T11:00:00Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'break',
|
||||
start: '2026-04-10T09:30:00Z',
|
||||
end: '2026-04-10T10:30:00Z',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a break longer than the work entry', async () => {
|
||||
const work = entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', { id: 'w1' });
|
||||
const { bp } = setup([work]);
|
||||
|
||||
await bp.placeBreak(breakRow, 0, 3 * HOUR);
|
||||
await bp.applyBreakPlacement('2026-04-10T09:30:00Z', 3 * HOUR);
|
||||
|
||||
const created = apiMocks.createTimeEntry.mock.calls.map((c) => c[0]);
|
||||
expect(created).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'work',
|
||||
start: '2026-04-10T12:30:00Z',
|
||||
end: '2026-04-10T13:00:00Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'break',
|
||||
start: '2026-04-10T09:30:00Z',
|
||||
end: '2026-04-10T12:30:00Z',
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(addNotification).toHaveBeenCalledWith('success', 'Break added', expect.any(String));
|
||||
});
|
||||
|
||||
it('moves an existing break out of the way of the pushed-out work', async () => {
|
||||
const work = entry('2026-04-10T09:00:00Z', '2026-04-10T12:00:00Z', { id: 'w1' });
|
||||
const existingBreak = entry('2026-04-10T12:00:00Z', '2026-04-10T12:15:00Z', {
|
||||
@@ -554,13 +419,6 @@ describe('useBreakPlacement.applyBreakPlacement (split)', () => {
|
||||
);
|
||||
expect(bp.breakPlacementRequest.value).toBeNull();
|
||||
});
|
||||
|
||||
it('does nothing when there is no pending placement request', async () => {
|
||||
const { bp, updateEntry } = setup([]);
|
||||
await bp.applyBreakPlacement('2026-04-10T12:00:00Z', HOUR);
|
||||
expect(updateEntry).not.toHaveBeenCalled();
|
||||
expect(apiMocks.createTimeEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBreakPlacement.applyBreakPlacement (move)', () => {
|
||||
|
||||
@@ -362,40 +362,6 @@ describe('useTimesheetCellMutations.handleCellUpdate', () => {
|
||||
expect(apiMocks.createTimeEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps an edited break anchored to its position instead of recentering', async () => {
|
||||
// 09-10 and 11:30-12:30 leave a 90-min gap; a resized 1h break has a valid
|
||||
// window of 10:00-10:30. The break already starts at 10:00, so it must stay
|
||||
// there (10:00-11:00) rather than jump to the centered 10:15-11:15.
|
||||
const morning = entry('2026-04-10T09:00:00Z', '2026-04-10T10:00:00Z', {
|
||||
id: 'morning',
|
||||
type: 'work',
|
||||
});
|
||||
const afternoon = entry('2026-04-10T11:30:00Z', '2026-04-10T12:30:00Z', {
|
||||
id: 'afternoon',
|
||||
type: 'work',
|
||||
});
|
||||
const existingBreak = entry('2026-04-10T10:00:00Z', '2026-04-10T10:30:00Z', {
|
||||
id: 'break-1',
|
||||
project_id: null,
|
||||
type: 'break',
|
||||
});
|
||||
const row = buildRow(null, [existingBreak], 'break-row');
|
||||
row.type = 'break';
|
||||
const { cellMutations } = setup([morning, afternoon, existingBreak]);
|
||||
|
||||
await cellMutations.handleCellUpdate(row, 0, HOUR);
|
||||
|
||||
expect(apiMocks.updateTimeEntry).toHaveBeenCalledTimes(1);
|
||||
expect(firstArg(apiMocks.updateTimeEntry)).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'break-1',
|
||||
start: '2026-04-10T10:00:00Z',
|
||||
end: '2026-04-10T11:00:00Z',
|
||||
})
|
||||
);
|
||||
expect(apiMocks.createTimeEntry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('grows a multi-break cell by re-placing the latest break, not fragmenting', async () => {
|
||||
// Two breaks share the break cell. Growing the cell total must extend the
|
||||
// latest-ending break (break-b) in place — never create a third break entry.
|
||||
|
||||
Reference in New Issue
Block a user