diff --git a/e2e/timesheet.spec.ts b/e2e/timesheet.spec.ts index 37a2729d..eaa64acf 100644 --- a/e2e/timesheet.spec.ts +++ b/e2e/timesheet.spec.ts @@ -717,111 +717,6 @@ test('test that adding a timesheet break to a full day splits the work entry via ]); }); -test('test that inserting a break as long as the work keeps the tracked work time', async ({ - page, - ctx, -}) => { - // A one-hour break splits but does not reduce the one-hour work entry. - await updateOrganizationSettingViaApi(ctx, { breaks_enabled: true }); - const day = getCurrentWeekMonday().toISOString().slice(0, 10); - await createTimeEntryWithTimestampsViaApi(ctx, { - start: `${day}T09:00:00Z`, - end: `${day}T10:00:00Z`, - description: 'One hour', - }); - - await goToTimesheet(page); - await expect(page.getByTestId('timesheet_view')).toBeVisible(); - - const breakCell = await fillBreakCell(page, '1'); - await breakCell.press('Enter'); - - await expect(page.getByTestId('break_placement_summary')).toBeVisible(); - await expect(page.getByTestId('break_placement_infeasible')).not.toBeVisible(); - await Promise.all([ - waitForBreakCreated(page), - page.getByRole('button', { name: 'Add break' }).click(), - ]); - - const dayEntries = await getDayEntriesViaApi(ctx, day); - expect(dayEntries.map((e) => [e.type, e.start, e.end])).toEqual([ - ['work', `${day}T09:00:00Z`, `${day}T09:30:00Z`], - ['break', `${day}T09:30:00Z`, `${day}T10:30:00Z`], - ['work', `${day}T10:30:00Z`, `${day}T11:00:00Z`], - ]); - const workRow = page - .locator('[data-testid="timesheet_row"]') - .filter({ hasNot: page.getByText('Break', { exact: true }) }) - .first(); - await expect( - workRow.locator('[data-testid="timesheet_cell"]').nth(0).locator('input') - ).toHaveValue('1h 00min'); -}); - -test('test that a late work entry starts earlier so the break still fits the day', async ({ - page, - ctx, -}) => { - // Move the block earlier rather than extending it into the next day. - await updateOrganizationSettingViaApi(ctx, { breaks_enabled: true }); - const day = getCurrentWeekMonday().toISOString().slice(0, 10); - await createTimeEntryWithTimestampsViaApi(ctx, { - start: `${day}T22:00:00Z`, - end: `${day}T23:30:00Z`, - description: 'Late shift', - }); - - await goToTimesheet(page); - await expect(page.getByTestId('timesheet_view')).toBeVisible(); - - const breakCell = await fillBreakCell(page, '1'); - await breakCell.press('Enter'); - - await expect(page.getByTestId('break_placement_summary')).toBeVisible(); - await expect(page.getByTestId('break_placement_infeasible')).not.toBeVisible(); - await Promise.all([ - waitForBreakCreated(page), - page.getByRole('button', { name: 'Add break' }).click(), - ]); - - const dayEntries = await getDayEntriesViaApi(ctx, day); - const nextDay = new Date(`${day}T00:00:00Z`); - nextDay.setUTCDate(nextDay.getUTCDate() + 1); - const midnight = `${nextDay.toISOString().slice(0, 10)}T00:00:00Z`; - expect(dayEntries.map((e) => [e.type, e.start, e.end])).toEqual([ - ['work', `${day}T21:30:00Z`, `${day}T22:15:00Z`], - ['break', `${day}T22:15:00Z`, `${day}T23:15:00Z`], - ['work', `${day}T23:15:00Z`, midnight], - ]); -}); - -test('test that a work entry too short to split gets the break placed next to it', async ({ - page, - ctx, -}) => { - // The one-minute work entry is too short to split. - await updateOrganizationSettingViaApi(ctx, { breaks_enabled: true }); - const day = getCurrentWeekMonday().toISOString().slice(0, 10); - await createTimeEntryWithTimestampsViaApi(ctx, { - start: `${day}T09:00:00Z`, - end: `${day}T09:01:00Z`, - description: 'Quick note', - }); - - await goToTimesheet(page); - await expect(page.getByTestId('timesheet_view')).toBeVisible(); - - const breakCell = await fillBreakCell(page, '0.5'); - await Promise.all([waitForBreakCreated(page), breakCell.press('Enter')]); - - await expect(page.getByTestId('break_placement_summary')).not.toBeVisible(); - const dayEntries = await getDayEntriesViaApi(ctx, day); - expect(dayEntries.map((e) => [e.type, e.start, e.end])).toEqual([ - ['work', `${day}T09:00:00Z`, `${day}T09:01:00Z`], - ['break', `${day}T09:01:00Z`, `${day}T09:31:00Z`], - ]); -}); - test('test that adding a break into an oversized gap places it without moving other entries', async ({ page, ctx, @@ -917,44 +812,12 @@ test('test that the placement modal warns when the chosen time would leave the b ['work', `${day}T12:00:00Z`, `${day}T17:00:00Z`], ]); // ...and the timesheet now shows the misaligned-break hint for that day - await expect( - page.getByRole('button', { name: 'does not align with your work entries' }) - ).toBeVisible(); -}); - -test('test that a misplaced break shows a warning on its timesheet day cell', async ({ - page, - ctx, -}) => { - // Work ends at 10:00 and the break starts hours later with no work after it, - // so it is misplaced and its day header should carry the warning hint. - await updateOrganizationSettingViaApi(ctx, { breaks_enabled: true }); - const day = getCurrentWeekMonday().toISOString().slice(0, 10); - await createTimeEntryWithTimestampsViaApi(ctx, { - start: `${day}T09:00:00Z`, - end: `${day}T10:00:00Z`, - }); - await createTimeEntryWithTimestampsViaApi(ctx, { - start: `${day}T14:00:00Z`, - end: `${day}T14:30:00Z`, - type: 'break', - }); - - await goToTimesheet(page); - await expect(page.getByTestId('timesheet_view')).toBeVisible(); - - // Exactly one warning, sitting in Monday's day header const hint = page.getByRole('button', { name: 'does not align with your work entries', }); - await expect(hint).toHaveCount(1); - await expect( - page.getByTestId('timesheet_day_header').first().getByRole('button', { - name: 'does not align with your work entries', - }) - ).toBeVisible(); + await expect(hint).toBeVisible(); - // The hint links to the calendar on the affected date + // The resulting warning links to the calendar on the affected date. await hint.click(); await expect(page.getByRole('link', { name: 'Fix in calendar' })).toHaveAttribute( 'href', diff --git a/resources/js/packages/ui/src/utils/breakPlacement.test.ts b/resources/js/packages/ui/src/utils/breakPlacement.test.ts index f19d1818..bee4cd37 100644 --- a/resources/js/packages/ui/src/utils/breakPlacement.test.ts +++ b/resources/js/packages/ui/src/utils/breakPlacement.test.ts @@ -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(); }); }); diff --git a/resources/js/utils/timesheet/breakPlacementMath.test.ts b/resources/js/utils/timesheet/breakPlacementMath.test.ts index cc78ac96..fff68d0a 100644 --- a/resources/js/utils/timesheet/breakPlacementMath.test.ts +++ b/resources/js/utils/timesheet/breakPlacementMath.test.ts @@ -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. diff --git a/resources/js/utils/timesheet/useBreakPlacement.test.ts b/resources/js/utils/timesheet/useBreakPlacement.test.ts index 1da80266..b98e9bca 100644 --- a/resources/js/utils/timesheet/useBreakPlacement.test.ts +++ b/resources/js/utils/timesheet/useBreakPlacement.test.ts @@ -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)', () => { diff --git a/resources/js/utils/timesheet/useTimesheetCellMutations.test.ts b/resources/js/utils/timesheet/useTimesheetCellMutations.test.ts index 77f813c7..94d41023 100644 --- a/resources/js/utils/timesheet/useTimesheetCellMutations.test.ts +++ b/resources/js/utils/timesheet/useTimesheetCellMutations.test.ts @@ -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.