fix(web): prevent search hotkey hydration mismatch (#39574)

This commit is contained in:
yyh 2026-07-26 15:08:48 +08:00 committed by GitHub
parent 448e378fc0
commit 99c1cb7781
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 2 deletions

View File

@ -0,0 +1,34 @@
import { act, waitFor, within } from '@testing-library/react'
import { hydrateRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
import { MainNavSearchButton } from '../search-button'
describe('MainNavSearchButton', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('renders Ctrl during SSR and corrects it after Mac hydration without a mismatch', async () => {
vi.spyOn(window.navigator, 'platform', 'get').mockReturnValue('MacIntel')
const app = <MainNavSearchButton />
const container = document.createElement('div')
container.innerHTML = renderToString(app)
const getSearchButton = () =>
within(container).getByRole('button', { name: 'app.gotoAnything.searchTitle' })
expect(getSearchButton()).toHaveTextContent('CtrlK')
expect(getSearchButton()).not.toHaveTextContent('⌘')
const onRecoverableError = vi.fn()
const root = hydrateRoot(container, app, {
onRecoverableError,
})
try {
await waitFor(() => expect(getSearchButton()).toHaveTextContent('⌘K'))
expect(onRecoverableError).not.toHaveBeenCalled()
} finally {
act(() => root.unmount())
}
})
})

View File

@ -2,13 +2,31 @@
import { DialogTrigger } from '@langgenius/dify-ui/dialog'
import { Kbd } from '@langgenius/dify-ui/kbd'
import { formatForDisplay } from '@tanstack/react-hotkeys'
import { detectPlatform, formatForDisplay } from '@tanstack/react-hotkeys'
import { useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle'
import { GOTO_ANYTHING_HOTKEY } from '@/app/components/goto-anything/hotkeys'
function noopSubscribe() {
return () => {}
}
function getPlatformSnapshot() {
return detectPlatform()
}
function getServerPlatformSnapshot(): ReturnType<typeof detectPlatform> {
return 'linux'
}
function useDisplayPlatform() {
return useSyncExternalStore(noopSubscribe, getPlatformSnapshot, getServerPlatformSnapshot)
}
export function MainNavSearchButton() {
const { t } = useTranslation()
const displayPlatform = useDisplayPlatform()
return (
<DialogTrigger
@ -24,7 +42,7 @@ export function MainNavSearchButton() {
<span aria-hidden className="i-custom-vender-main-nav-quick-search h-4 w-4" />
<Kbd className="h-4.5 min-w-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
{GOTO_ANYTHING_HOTKEY.split('+').map((key) => (
<span key={key}>{formatForDisplay(key)}</span>
<span key={key}>{formatForDisplay(key, { platform: displayPlatform })}</span>
))}
</Kbd>
</DialogTrigger>