Two things Steve asked for, both in the #99 Plants mode: - A "Recent" strip of the plants most recently planted IN THIS GARDEN (recentlyPlantedIds — derived from actual plantings, newest first, unique; NOT the manual localStorage tray), as tap-to-arm chips. So re-planting "more of the same" is one tap, no picker. Hidden until something's planted. - A clump/rows fill control (FillControl), shown once a plant is armed: pick a layout, "Fill bed", and it runs POST /objects/:id/fill region=all with the chosen layout — the #77 grid/clump fill the UI could NOT reach before (it was agent/REST only). Defaults to rows (a real planting). useFillObject mirrors useClearObject: one request, invalidate /full. Both live in the shared PlantPlacementTools, so desktop's focus toolbar and the mobile Plants strip get them identically. Fill is capped/validated server-side (#95) and covered spots are skipped, so re-filling is safe. Verified live at 390px: the Recent strip shows this garden's tomato/lettuce/ garlic; arming a chip reveals Clump|Rows + Fill bed; fill fires cleanly. tsc + vitest (95, incl. recentlyPlantedIds) + build green. DESIGN updated. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { computeDerivedCount, effectiveCount, recentlyPlantedIds, type EditorPlanting } from './plantings'
|
|
|
|
function plop(over: Partial<EditorPlanting>): EditorPlanting {
|
|
return {
|
|
id: 0,
|
|
objectId: 1,
|
|
plantId: 1,
|
|
xCm: 0,
|
|
yCm: 0,
|
|
radiusCm: 5,
|
|
count: null,
|
|
derivedCount: 1,
|
|
label: null,
|
|
plantedAt: null,
|
|
version: 1,
|
|
...over,
|
|
}
|
|
}
|
|
|
|
describe('recentlyPlantedIds', () => {
|
|
it('returns unique plant ids, newest planted first', () => {
|
|
const got = recentlyPlantedIds([
|
|
plop({ id: 1, plantId: 10, plantedAt: '2026-05-01' }),
|
|
plop({ id: 2, plantId: 20, plantedAt: '2026-07-01' }),
|
|
plop({ id: 3, plantId: 10, plantedAt: '2026-06-01' }), // dup plant, later
|
|
])
|
|
// 20 (Jul) before 10 (its most recent plop is Jun); 10 appears once.
|
|
expect(got).toEqual([20, 10])
|
|
})
|
|
|
|
it('breaks a same-day tie by newer plop id, and sorts undated last', () => {
|
|
const got = recentlyPlantedIds([
|
|
plop({ id: 5, plantId: 30, plantedAt: null }),
|
|
plop({ id: 6, plantId: 40, plantedAt: '2026-07-01' }),
|
|
plop({ id: 7, plantId: 50, plantedAt: '2026-07-01' }),
|
|
])
|
|
expect(got).toEqual([50, 40, 30]) // id 7 > 6 on the same day; undated 30 last
|
|
})
|
|
|
|
it('is empty for no plantings', () => {
|
|
expect(recentlyPlantedIds([])).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('computeDerivedCount', () => {
|
|
it('mirrors the server formula max(1, round(π·r²/spacing²))', () => {
|
|
expect(computeDerivedCount(10, 10)).toBe(3) // π·100/100 = 3.14 → 3
|
|
expect(computeDerivedCount(50, 10)).toBe(79) // π·2500/100 = 78.5 → 79
|
|
expect(computeDerivedCount(30, 15)).toBe(13) // π·900/225 = 12.57 → 13
|
|
})
|
|
|
|
it('floors at 1 for tiny / non-positive inputs', () => {
|
|
expect(computeDerivedCount(0.1, 10)).toBe(1)
|
|
expect(computeDerivedCount(0, 10)).toBe(1)
|
|
expect(computeDerivedCount(10, 0)).toBe(1)
|
|
})
|
|
|
|
it('caps like the server', () => {
|
|
expect(computeDerivedCount(10_000, 0.1)).toBe(1_000_000)
|
|
})
|
|
})
|
|
|
|
describe('effectiveCount', () => {
|
|
it('prefers the override, else the derived value', () => {
|
|
expect(effectiveCount({ count: 12, derivedCount: 79 })).toBe(12)
|
|
expect(effectiveCount({ count: null, derivedCount: 79 })).toBe(79)
|
|
})
|
|
})
|