7c94d99b9f
Frontend pattern-consistency sweep (note #1026, the last DRY-thread item). TagMaintenanceCard (4 flows) + PostMaintenanceCard (2 flows) each hand-rolled the same sync preview→commit state machine: a previewData/previewing/committing triple + onPreview/onCommit that dry-run-previews, then applies and collapses the projection (the apply shares the backend predicate, so afterward it's empty). Extract usePreviewCommit({preview, commit, emptyPreview}) owning that lifecycle. The 6 flows become declarative: supply the two thunks + the collapse shape. The normalize flow (commit dispatches a self-resuming background task, not a sync apply) omits emptyPreview so the projection stays and a truthy result = queued. Composable returns are aliased to the cards' existing local names, so the templates only change where they read the apply result (the success badges). Long-Celery-task cards (GatedPurge/VideoDedup) keep useMaintenanceTask — a different pattern (navigable-away task lifecycle), deliberately not merged. Exhaustiveness: no card hand-rolls the refs anymore; the only dryRun:false callers are these two cards, both via the composable. Added a vitest spec for the primitive (collapse static + fn, dispatch-variant, re-preview clears result). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
2.4 KiB
JavaScript
67 lines
2.4 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import { usePreviewCommit } from '../src/composables/usePreviewCommit.js'
|
|
|
|
// Canonical sync preview→commit flow for maintenance tiles (DRY pattern sweep,
|
|
// #753) — the primitive TagMaintenanceCard + PostMaintenanceCard's 6 flows route
|
|
// through.
|
|
|
|
describe('usePreviewCommit', () => {
|
|
it('runPreview loads the projection and toggles previewing', async () => {
|
|
const pc = usePreviewCommit({
|
|
preview: async () => ({ count: 3 }),
|
|
commit: async () => ({ deleted: 3 }),
|
|
})
|
|
expect(pc.previewData.value).toBe(null)
|
|
const p = pc.runPreview()
|
|
expect(pc.previewing.value).toBe(true)
|
|
await p
|
|
expect(pc.previewing.value).toBe(false)
|
|
expect(pc.previewData.value).toEqual({ count: 3 })
|
|
})
|
|
|
|
it('runCommit stores the result and collapses to a static emptyPreview', async () => {
|
|
const pc = usePreviewCommit({
|
|
preview: async () => ({ count: 3 }),
|
|
commit: async () => ({ deleted: 3 }),
|
|
emptyPreview: { count: 0, sample_names: [] },
|
|
})
|
|
await pc.runPreview()
|
|
await pc.runCommit()
|
|
expect(pc.result.value).toEqual({ deleted: 3 })
|
|
expect(pc.previewData.value).toEqual({ count: 0, sample_names: [] })
|
|
})
|
|
|
|
it('emptyPreview as a function derives the collapsed shape from the result', async () => {
|
|
const pc = usePreviewCommit({
|
|
preview: async () => ({ count: 2 }),
|
|
commit: async () => ({ sample_names: ['a', 'b'] }),
|
|
emptyPreview: (r) => ({ count: 0, sample_names: r.sample_names }),
|
|
})
|
|
await pc.runCommit()
|
|
expect(pc.previewData.value).toEqual({ count: 0, sample_names: ['a', 'b'] })
|
|
})
|
|
|
|
it('without emptyPreview the projection stays (dispatch-on-commit variant)', async () => {
|
|
const pc = usePreviewCommit({
|
|
preview: async () => ({ total_changes: 5 }),
|
|
commit: async () => ({ status: 'queued' }),
|
|
})
|
|
await pc.runPreview()
|
|
await pc.runCommit()
|
|
expect(pc.result.value).toEqual({ status: 'queued' })
|
|
expect(pc.previewData.value).toEqual({ total_changes: 5 }) // unchanged
|
|
})
|
|
|
|
it('runPreview clears a prior apply result (re-preview hides the badge)', async () => {
|
|
const pc = usePreviewCommit({
|
|
preview: async () => ({ count: 1 }),
|
|
commit: async () => ({ deleted: 1 }),
|
|
emptyPreview: { count: 0 },
|
|
})
|
|
await pc.runCommit()
|
|
expect(pc.result.value).toEqual({ deleted: 1 })
|
|
await pc.runPreview()
|
|
expect(pc.result.value).toBe(null)
|
|
})
|
|
})
|