mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-23 22:09:56 +00:00
fix(web): hide unavailable history actions and reorder message actions (#1494)
* fix(web): reorder and hide history actions * test(web): cover locked history actions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import type { ComponentProps, PropsWithChildren } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
@@ -13,6 +13,7 @@ const auiState = {
|
||||
message: { id: 'msg-1', createdAt: new Date(2026, 6, 12, 10, 30) },
|
||||
thread: {
|
||||
isRunning: false,
|
||||
messages: [],
|
||||
extras: {
|
||||
shareHiddenByMessageId: new Set<string>(),
|
||||
},
|
||||
@@ -50,6 +51,10 @@ vi.mock('@/hooks/useCopyToClipboard', () => ({
|
||||
useCopyToClipboard: () => ({ copied: false, copy })
|
||||
}))
|
||||
|
||||
vi.mock('@/components/AssistantChat/context', () => ({
|
||||
useOptionalHappyChatContext: () => ({ onShareTurn: vi.fn() })
|
||||
}))
|
||||
|
||||
function renderActions(props: ComponentProps<typeof MessageActions>) {
|
||||
return render(
|
||||
<I18nProvider>
|
||||
@@ -211,6 +216,85 @@ describe('MessageActions', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('hides Fork and Rewind while the thread is running', () => {
|
||||
auiState.thread.isRunning = true
|
||||
|
||||
renderActions({
|
||||
align: 'end',
|
||||
copyText: 'body',
|
||||
showFork: true,
|
||||
showRewind: true,
|
||||
onFork: async () => {},
|
||||
onRewind: async () => {}
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Fork' })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: 'Rewind' })).toBeNull()
|
||||
})
|
||||
|
||||
it('hides Fork and Rewind while a history action is pending', () => {
|
||||
renderActions({
|
||||
align: 'end',
|
||||
copyText: 'body',
|
||||
showFork: true,
|
||||
showRewind: true,
|
||||
historyActionPending: true,
|
||||
onFork: async () => {},
|
||||
onRewind: async () => {}
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Fork' })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: 'Rewind' })).toBeNull()
|
||||
})
|
||||
|
||||
it('hides all history actions while a confirmation is pending', async () => {
|
||||
let resolveFork: (() => void) | undefined
|
||||
const onFork = vi.fn(() => new Promise<void>((resolve) => {
|
||||
resolveFork = resolve
|
||||
}))
|
||||
|
||||
renderActions({
|
||||
align: 'end',
|
||||
copyText: 'body',
|
||||
showFork: true,
|
||||
showRewind: true,
|
||||
onFork,
|
||||
onRewind: async () => {}
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Fork' }))
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Fork' }).at(-1)!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.happy-message-actions')?.querySelectorAll('button')).toHaveLength(1)
|
||||
})
|
||||
expect(screen.queryByRole('button', { name: 'Rewind' })).toBeNull()
|
||||
|
||||
resolveFork?.()
|
||||
await waitFor(() => expect(onFork).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('orders user actions as Share, Rewind, Fork, Copy', () => {
|
||||
renderActions({
|
||||
align: 'end',
|
||||
copyText: 'body',
|
||||
messageElementId: 'message-1',
|
||||
showFork: true,
|
||||
showRewind: true,
|
||||
onFork: async () => {},
|
||||
onRewind: async () => {}
|
||||
})
|
||||
|
||||
const row = document.querySelector('.happy-message-actions')
|
||||
expect(row).not.toBeNull()
|
||||
expect(Array.from(row!.querySelectorAll('button')).map((button) => button.getAttribute('aria-label'))).toEqual([
|
||||
'Share turn as image',
|
||||
'Rewind',
|
||||
'Fork',
|
||||
'Copy'
|
||||
])
|
||||
})
|
||||
|
||||
it('shows Fork confirm dialog and calls onFork only after confirm', async () => {
|
||||
const onFork = vi.fn(async () => {})
|
||||
renderActions({ align: 'start', copyText: 'body', showFork: true, onFork })
|
||||
|
||||
@@ -88,34 +88,44 @@ export function MessageActions({
|
||||
/>
|
||||
) : null
|
||||
|
||||
const historyButtons = (
|
||||
const historyButtons = !actionsLocked ? (
|
||||
<>
|
||||
{showFork && onFork ? (
|
||||
<button
|
||||
type="button"
|
||||
title={t('message.fork')}
|
||||
aria-label={t('message.fork')}
|
||||
disabled={actionsLocked}
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] disabled:opacity-40"
|
||||
onClick={() => setForkOpen(true)}
|
||||
>
|
||||
<ForkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
{showRewind && onRewind ? (
|
||||
<button
|
||||
type="button"
|
||||
title={t('message.rewind')}
|
||||
aria-label={t('message.rewind')}
|
||||
disabled={actionsLocked}
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] disabled:opacity-40"
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
|
||||
onClick={() => setRewindOpen(true)}
|
||||
>
|
||||
<RewindIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
{showFork && onFork ? (
|
||||
<button
|
||||
type="button"
|
||||
title={t('message.fork')}
|
||||
aria-label={t('message.fork')}
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
|
||||
onClick={() => setForkOpen(true)}
|
||||
>
|
||||
<ForkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
|
||||
const copyButton = canCopy ? (
|
||||
<button
|
||||
type="button"
|
||||
title={copied ? t('message.copied') : t('message.copy')}
|
||||
aria-label={copied ? t('message.copied') : t('message.copy')}
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
|
||||
onClick={() => copy(copyText!)}
|
||||
>
|
||||
{copied ? <CheckIcon className="h-3.5 w-3.5 text-green-500" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -128,18 +138,10 @@ export function MessageActions({
|
||||
{align === 'end' ? <DesktopTimestamp /> : null}
|
||||
{align === 'end' && hasMetadata && metadata ? <MessageInfoPopover metadata={metadata} /> : null}
|
||||
{align === 'end' ? shareButton : null}
|
||||
{canCopy ? (
|
||||
<button
|
||||
type="button"
|
||||
title={copied ? t('message.copied') : t('message.copy')}
|
||||
aria-label={copied ? t('message.copied') : t('message.copy')}
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
|
||||
onClick={() => copy(copyText!)}
|
||||
>
|
||||
{copied ? <CheckIcon className="h-3.5 w-3.5 text-green-500" /> : <CopyIcon className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
) : null}
|
||||
{historyButtons}
|
||||
{align === 'end' ? historyButtons : null}
|
||||
{align === 'end' ? copyButton : null}
|
||||
{align === 'start' ? copyButton : null}
|
||||
{align === 'start' ? historyButtons : null}
|
||||
{align === 'start' ? shareButton : null}
|
||||
{align === 'start' && hasMetadata && metadata ? <MessageInfoPopover metadata={metadata} /> : null}
|
||||
{align === 'start' ? <DesktopTimestamp /> : null}
|
||||
|
||||
Reference in New Issue
Block a user