AI: address review on PR #470
このコミットが含まれているのは:
@@ -12,24 +12,36 @@ const api = vi.hoisted (() => ({
|
||||
vi.mock ('@/lib/api', () => api)
|
||||
|
||||
describe ('PostFormTagsArea', () => {
|
||||
it ('updates text and fetches autocomplete for the selected token', async () => {
|
||||
it ('replaces the selected token and restores the caret without losing other tags', async () => {
|
||||
const setTags = vi.fn ()
|
||||
api.apiGet.mockResolvedValueOnce ([buildTag ({ name: '虹夏', postCount: 3 })])
|
||||
|
||||
renderWithProviders (<PostFormTagsArea tags="虹" setTags={setTags}/>)
|
||||
const { rerender } = renderWithProviders (
|
||||
<PostFormTagsArea tags="ぼっち 虹 後続" setTags={setTags}/>)
|
||||
|
||||
const textarea = screen.getByRole ('textbox')
|
||||
const textarea = screen.getByRole<HTMLTextAreaElement> ('textbox')
|
||||
fireEvent.focus (textarea)
|
||||
fireEvent.select (textarea, { target: { selectionStart: 1, selectionEnd: 1 } })
|
||||
fireEvent.change (textarea, { target: { value: '虹夏' } })
|
||||
fireEvent.change (textarea, { target: { value: 'ぼっち 虹な 後続' } })
|
||||
rerender (<PostFormTagsArea tags="ぼっち 虹な 後続" setTags={setTags}/>)
|
||||
fireEvent.select (textarea, { target: { selectionStart: 6, selectionEnd: 6 } })
|
||||
|
||||
await waitFor (() => {
|
||||
expect (api.apiGet).toHaveBeenCalledWith (
|
||||
'/tags/autocomplete',
|
||||
{ locale: 'ja', params: { q: '虹', nico: '0' },
|
||||
signal: expect.any (AbortSignal) },
|
||||
)
|
||||
'/tags/autocomplete', {
|
||||
locale: 'ja', params: { q: '虹な', nico: '0' },
|
||||
signal: expect.any (AbortSignal) })
|
||||
})
|
||||
expect (setTags).toHaveBeenCalledWith ('虹夏')
|
||||
expect (setTags).toHaveBeenCalledWith ('ぼっち 虹な 後続')
|
||||
|
||||
fireEvent.mouseDown (await screen.findByText ('虹夏'))
|
||||
expect (setTags).toHaveBeenLastCalledWith ('ぼっち 虹夏 後続')
|
||||
rerender (<PostFormTagsArea tags="ぼっち 虹夏 後続" setTags={setTags}/>)
|
||||
|
||||
await waitFor (() => {
|
||||
expect (textarea).toHaveFocus ()
|
||||
expect (textarea.selectionStart).toBe (7)
|
||||
expect (textarea.selectionEnd).toBe (7)
|
||||
})
|
||||
expect (screen.queryByText ('虹夏')).not.toBeInTheDocument ()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { act, fireEvent, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -8,6 +8,8 @@ import * as api from '@/lib/api'
|
||||
import { buildTag } from '@/test/factories'
|
||||
import { renderWithProviders } from '@/test/render'
|
||||
|
||||
import type { Tag } from '@/types'
|
||||
|
||||
|
||||
const LocationOutput = () => {
|
||||
const location = useLocation ()
|
||||
@@ -117,4 +119,39 @@ describe ('TagSearch', () => {
|
||||
expect (screen.getByTestId ('location')).toHaveTextContent ('/ja/posts?page=3')
|
||||
expect (screen.queryByText ('虹夏')).not.toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it ('aborts obsolete requests and ignores their late suggestions', async () => {
|
||||
const responses: ((tags: Tag[]) => void)[] = []
|
||||
vi.mocked (api.apiGet).mockImplementation (() => new Promise<Tag[]> (resolve => {
|
||||
responses.push (resolve)
|
||||
}))
|
||||
const { unmount } = renderSearch ()
|
||||
const input = screen.getByRole ('textbox')
|
||||
|
||||
fireEvent.change (input, { target: { value: '虹' } })
|
||||
const previousSignal = vi.mocked (api.apiGet).mock.calls.at (-1)?.[1]?.signal
|
||||
expect (previousSignal).toBeInstanceOf (AbortSignal)
|
||||
expect (previousSignal?.aborted).toBe (false)
|
||||
|
||||
fireEvent.change (input, { target: { value: 'ぼ' } })
|
||||
expect (previousSignal?.aborted).toBe (true)
|
||||
expect (api.apiGet).toHaveBeenLastCalledWith ('/tags/autocomplete', {
|
||||
locale: 'ja', params: { q: 'ぼ', nico: '1' },
|
||||
signal: expect.any (AbortSignal) })
|
||||
const currentSignal = vi.mocked (api.apiGet).mock.calls.at (-1)?.[1]?.signal
|
||||
|
||||
await act (async () => {
|
||||
responses[1] ([buildTag ({ name: 'ぼっち' })])
|
||||
})
|
||||
expect (screen.getByText ('ぼっち')).toBeInTheDocument ()
|
||||
|
||||
await act (async () => {
|
||||
responses[0] ([buildTag ({ name: '虹夏' })])
|
||||
})
|
||||
expect (screen.queryByText ('虹夏')).not.toBeInTheDocument ()
|
||||
expect (screen.getByText ('ぼっち')).toBeInTheDocument ()
|
||||
|
||||
unmount ()
|
||||
expect (currentSignal?.aborted).toBe (true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import TagInput from '@/components/common/TagInput'
|
||||
@@ -20,18 +20,20 @@ describe ('TagInput', () => {
|
||||
const setValue = vi.fn ()
|
||||
api.apiGet.mockResolvedValueOnce ([buildTag ({ name: '虹夏', postCount: 2 })])
|
||||
|
||||
render (<TagInput value="ぼっち 虹" setValue={setValue}/>)
|
||||
const { rerender } = render (
|
||||
<TagInput value="ぼっち 虹" setValue={setValue}/>)
|
||||
|
||||
fireEvent.change (screen.getByRole ('textbox'), { target: { value: 'ぼっち 虹夏' } })
|
||||
rerender (<TagInput value="ぼっち 虹夏" setValue={setValue}/>)
|
||||
|
||||
await waitFor (() => {
|
||||
expect (api.apiGet).toHaveBeenCalledWith (
|
||||
'/tags/autocomplete',
|
||||
{ locale: 'ja', params: { q: '虹夏', nico: '0' },
|
||||
signal: expect.any (AbortSignal) },
|
||||
)
|
||||
'/tags/autocomplete', {
|
||||
locale: 'ja', params: { q: '虹夏', nico: '0' },
|
||||
signal: expect.any (AbortSignal) })
|
||||
})
|
||||
expect (setValue).toHaveBeenCalledWith ('ぼっち 虹夏')
|
||||
expect (await screen.findByText ('虹夏')).toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it ('does not fetch when the last token is blank', () => {
|
||||
@@ -54,10 +56,51 @@ describe ('TagInput', () => {
|
||||
|
||||
await waitFor (() => {
|
||||
expect (api.apiGet).toHaveBeenCalledWith (
|
||||
'/tags/autocomplete',
|
||||
{ locale: 'ja', params: { q: '虹夏', nico: '1' },
|
||||
signal: expect.any (AbortSignal) },
|
||||
)
|
||||
'/tags/autocomplete', {
|
||||
locale: 'ja', params: { q: '虹夏', nico: '1' },
|
||||
signal: expect.any (AbortSignal) })
|
||||
})
|
||||
})
|
||||
|
||||
it ('preserves negation and blocks suggestion selection during composition', async () => {
|
||||
const setValue = vi.fn ()
|
||||
api.apiGet.mockResolvedValueOnce ([buildTag ({ name: '虹夏' })])
|
||||
const { rerender } = render (<TagInput value="" setValue={setValue}/>)
|
||||
const input = screen.getByRole ('textbox')
|
||||
|
||||
fireEvent.change (input, { target: { value: 'ぼっち NOT:虹' } })
|
||||
rerender (<TagInput value="ぼっち NOT:虹" setValue={setValue}/>)
|
||||
await screen.findByText ('虹夏')
|
||||
expect (api.apiGet).toHaveBeenLastCalledWith (
|
||||
'/tags/autocomplete', {
|
||||
locale: 'ja', params: { q: '虹', nico: '0' },
|
||||
signal: expect.any (AbortSignal) })
|
||||
|
||||
fireEvent.keyDown (input, { key: 'ArrowDown' })
|
||||
fireEvent.compositionStart (input)
|
||||
expect (fireEvent.keyDown (input, { key: 'Enter' })).toBe (false)
|
||||
expect (setValue).toHaveBeenCalledTimes (1)
|
||||
|
||||
fireEvent.compositionEnd (input)
|
||||
fireEvent.keyDown (input, { key: 'Enter' })
|
||||
|
||||
expect (setValue).toHaveBeenLastCalledWith ('ぼっち not:虹夏 ')
|
||||
expect (screen.queryByText ('虹夏')).not.toBeInTheDocument ()
|
||||
})
|
||||
|
||||
it.each ([false, true]) ('includes unused tags only with includeUnused=%s', async unusedFlg => {
|
||||
const tag = buildTag ({ name: '虹夏', postCount: 0 })
|
||||
const response = Promise.resolve ([tag])
|
||||
api.apiGet.mockReturnValueOnce (response)
|
||||
const props = { setValue: vi.fn (), includeUnused: unusedFlg }
|
||||
const { rerender } = render (<TagInput {...props} value=""/>)
|
||||
|
||||
fireEvent.change (screen.getByRole ('textbox'), { target: { value: '虹' } })
|
||||
rerender (<TagInput {...props} value="虹"/>)
|
||||
await act (async () => {
|
||||
await response
|
||||
})
|
||||
|
||||
expect (screen.queryByText ('虹夏') != null).toBe (unusedFlg)
|
||||
})
|
||||
})
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする