diff --git a/frontend/src/components/PostEditForm.test.tsx b/frontend/src/components/PostEditForm.test.tsx index 60c4170..395c789 100644 --- a/frontend/src/components/PostEditForm.test.tsx +++ b/frontend/src/components/PostEditForm.test.tsx @@ -1,11 +1,13 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import { act, fireEvent, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { textContentOf } from '@/test/text' import PostEditForm from '@/components/PostEditForm' import { buildPost, buildTag } from '@/test/factories' import { renderWithProviders as render } from '@/test/render' +import type { DialogueAPI } from '@/lib/dialogues/useDialogue' + const postsApi = vi.hoisted (() => ({ updatePost: vi.fn (), })) @@ -18,18 +20,27 @@ const toastApi = vi.hoisted (() => ({ toast: vi.fn (), })) +const dialogue = vi.hoisted (() => ({ + confirm: vi.fn (), + choice: vi.fn () })) + vi.mock ('@/lib/posts', () => postsApi) vi.mock ('@/lib/api', () => api) vi.mock ('@/components/ui/use-toast', () => toastApi) vi.mock ('@/lib/dialogues/useDialogue', async importOriginal => ({ - ...await importOriginal (), - default: () => ({ - choice: vi.fn (), - }), -})) + ...await importOriginal (), + default: () => dialogue })) describe ('PostEditForm', () => { + beforeEach (() => { + vi.resetAllMocks () + }) + it ('submits edited post fields with the current base version', async () => { + const confirm = vi.fn<(value: boolean) => void> () + dialogue.confirm.mockReturnValueOnce (new Promise (resolve => { + confirm.mockImplementation (resolve) + })) const onSave = vi.fn () const post = buildPost ({ id: 8, @@ -55,6 +66,16 @@ describe ('PostEditForm', () => { fireEvent.change (parentIds, { target: { value: '3 4' } }) fireEvent.submit (screen.getByRole ('button', { name: '更新' }).closest ('form')!) + expect (postsApi.updatePost).not.toHaveBeenCalled () + expect (dialogue.confirm).toHaveBeenCalledTimes (1) + const difference = textContentOf ( + dialogue.confirm.mock.calls[0][0].description) + expect (difference).toContain ('oldnew') + expect (difference).toContain ('23 4') + expect (difference).not.toContain ('general-tag') + + await act (async () => { confirm (true) }) + await waitFor (() => { expect (postsApi.updatePost).toHaveBeenCalledWith ( expect.objectContaining ({ @@ -71,6 +92,28 @@ describe ('PostEditForm', () => { expect (textContentOf (toastApi.toast.mock.calls[0][0].description)).toBe ('更新しました.') }) + it ('keeps edited fields without submitting when confirmation is cancelled', async () => { + const onSave = vi.fn () + dialogue.confirm.mockResolvedValueOnce (false) + render () + + const [title, parentIds] = screen.getAllByRole ('textbox') + fireEvent.change (title, { target: { value: 'new' } }) + fireEvent.change (parentIds, { target: { value: '3 4' } }) + const button = screen.getByRole ('button', { name: '更新' }) + fireEvent.submit (button.closest ('form')!) + + await waitFor (() => { + expect (button).toBeEnabled () + }) + expect (dialogue.confirm).toHaveBeenCalledTimes (1) + expect (postsApi.updatePost).not.toHaveBeenCalled () + expect (onSave).not.toHaveBeenCalled () + expect (toastApi.toast).not.toHaveBeenCalled () + expect (title).toHaveValue ('new') + expect (parentIds).toHaveValue ('3 4') + }) + it ('preserves duration while the video tag is temporarily removed', () => { const post = buildPost ({ videoMs: 180_500, @@ -100,6 +143,7 @@ describe ('PostEditForm', () => { 'shows deduplicated original-created endpoint errors on the shared datetime field', async () => { const post = buildPost () + dialogue.confirm.mockResolvedValueOnce (true) api.isApiError.mockReturnValue (true) postsApi.updatePost.mockRejectedValueOnce ({ response: {