fix(web): single-flight goal clear

This commit is contained in:
kingwl
2026-07-31 20:54:23 +08:00
parent 5c4b701afe
commit e73cdd6b7d
11 changed files with 148 additions and 34 deletions

View File

@@ -3,13 +3,13 @@
// inline edit form, and resume/clear icon actions — driven purely through
// props, no wire. Loading, absent, and complete goals render nothing.
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GoalSnapshot } from '@deepseek-ai/dsh-goal/client'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import { GoalBar } from '../src/client/GoalBar.tsx'
import type { GoalBarActions } from '../src/client/slots.ts'
import type { GoalActionResult, GoalBarActions } from '../src/client/slots.ts'
import { zh } from '../src/client/locales.ts'
// The framework-injected t seat, stubbed over the zh dictionaries (the default locale).
@@ -61,6 +61,27 @@ describe('GoalBar', () => {
expect(actions.onClear).toHaveBeenCalledTimes(1)
})
it('single-flights rapid clear clicks and hides the committed goal before its projection catches up', async () => {
const actions = makeActions()
let resolveClear!: (result: GoalActionResult) => void
actions.onClear.mockImplementation(() => new Promise((resolve) => { resolveClear = resolve }))
const { container, rerender } = render(<GoalBar goal={makeGoal()} {...actions} t={t} />)
const clear = screen.getByRole<HTMLButtonElement>('button', { name: '清除目标' })
act(() => {
clear.click()
clear.click()
})
expect(actions.onClear).toHaveBeenCalledTimes(1)
expect(clear.disabled).toBe(true)
await act(async () => { resolveClear({ ok: true }) })
expect(container.firstChild).toBeNull()
rerender(<GoalBar goal={makeGoal({ id: 'g2' as GoalSnapshot['id'], objective: 'Next goal' })} {...actions} t={t} />)
expect(screen.getByText('Next goal')).toBeTruthy()
})
it('edit swaps the strip for a prefilled form; Enter saves, empty stays disabled', async () => {
const actions = makeActions()
render(<GoalBar goal={makeGoal()} {...actions} t={t} />)
@@ -180,5 +201,7 @@ describe('GoalBar', () => {
fireEvent.click(screen.getByRole('button', { name: '清除目标' }))
expect((await screen.findByRole('alert')).textContent).toBe('clear failed (agent-busy)')
expect(screen.getByText('Ship the redesign')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: '清除目标' }))
await waitFor(() => { expect(actions.onClear).toHaveBeenCalledTimes(2) })
})
})