f1181e8510
Reviewed-on: #413 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
279 行
9.5 KiB
TypeScript
279 行
9.5 KiB
TypeScript
import { act, render, screen, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
|
|
import { buildPostImportRow } from '@/test/postImportFactories'
|
|
|
|
import type { DialogueFormAction, DialogueFormControls } from '@/lib/dialogues/useDialogue'
|
|
|
|
const api = vi.hoisted (() => ({
|
|
apiGet: vi.fn (),
|
|
}))
|
|
|
|
vi.mock ('@/lib/api', () => api)
|
|
|
|
describe ('PostImportRowForm', () => {
|
|
beforeEach (() => {
|
|
vi.clearAllMocks ()
|
|
globalThis.URL.createObjectURL = vi.fn (() => 'blob:preview')
|
|
globalThis.URL.revokeObjectURL = vi.fn ()
|
|
api.apiGet.mockResolvedValue (new Blob (['img'], { type: 'image/png' }))
|
|
})
|
|
|
|
it ('resets only the draft, then saves with resetRequested', async () => {
|
|
const row = buildPostImportRow ()
|
|
row.attributes.title = 'manual title'
|
|
row.provenance.title = 'manual'
|
|
const actions: DialogueFormAction[][] = []
|
|
const controls: DialogueFormControls = {
|
|
close: vi.fn (),
|
|
confirm: vi.fn ().mockResolvedValue (true),
|
|
setActions: next => actions.push (next) }
|
|
const invalidRow = buildPostImportRow ({
|
|
validationErrors: { title: ['タイトルを確認してください.'] } })
|
|
const onSave = vi.fn ().mockResolvedValue ({ saved: false, row: invalidRow })
|
|
|
|
render (<PostImportRowForm row={row} controls={controls} onSave={onSave}/>)
|
|
const titleInput = screen.getByDisplayValue ('manual title')
|
|
|
|
await waitFor (() => expect (actions.at (-1)?.length).toBe (2))
|
|
const reset = actions.at (-1)?.find (action => action.label === '変更をリセット')
|
|
expect (reset).toMatchObject ({ placement: 'start', variant: 'danger', disabled: false })
|
|
await act (async () => {
|
|
await reset?.onSelect ()
|
|
})
|
|
|
|
expect (controls.confirm).toHaveBeenCalled ()
|
|
expect (titleInput).toHaveValue ('')
|
|
expect (onSave).not.toHaveBeenCalled ()
|
|
|
|
const save = actions.at (-1)?.find (action => action.label === '編輯内容を保存')
|
|
await act (async () => {
|
|
await save?.onSelect ()
|
|
})
|
|
|
|
expect (onSave).toHaveBeenCalledWith ({
|
|
draft: expect.objectContaining ({ title: '' }),
|
|
resetRequested: true })
|
|
expect (screen.getByText ('タイトルを確認してください.')).toBeInTheDocument ()
|
|
})
|
|
|
|
it ('does not reset the draft when confirmation is cancelled', async () => {
|
|
const row = buildPostImportRow ({ attributes: { title: 'manual title' } })
|
|
let actions: DialogueFormAction[] = []
|
|
const controls: DialogueFormControls = {
|
|
close: vi.fn (),
|
|
confirm: vi.fn ().mockResolvedValue (false),
|
|
setActions: next => {
|
|
actions = next
|
|
} }
|
|
|
|
render (
|
|
<PostImportRowForm
|
|
row={row}
|
|
controls={controls}
|
|
onSave={vi.fn ()}/>)
|
|
await waitFor (() => expect (actions.length).toBe (2))
|
|
|
|
await act (async () => {
|
|
await actions.find (action => action.label === '変更をリセット')?.onSelect ()
|
|
})
|
|
|
|
expect (screen.getByDisplayValue ('manual title')).toBeInTheDocument ()
|
|
})
|
|
|
|
it ('marks edited fields and areas invalid from field errors', () => {
|
|
const row = buildPostImportRow ({
|
|
validationErrors: { url: ['URL error'], tags: ['tag error'] },
|
|
importErrors: { title: ['title error'] },
|
|
fieldWarnings: { title: ['title warning'] } })
|
|
|
|
render (
|
|
<PostImportRowForm
|
|
row={row}
|
|
controls={{ close: vi.fn (), confirm: vi.fn (), setActions: vi.fn () }}
|
|
onSave={vi.fn ()}/>)
|
|
|
|
expect (screen.getByText ('URL error')).toBeInTheDocument ()
|
|
expect (screen.getByText ('tag error')).toBeInTheDocument ()
|
|
expect (screen.getByText ('title error')).toBeInTheDocument ()
|
|
expect (screen.getByText ('title warning')).toBeInTheDocument ()
|
|
expect (screen.getAllByRole ('textbox').filter (
|
|
textbox => textbox.getAttribute ('aria-invalid') === 'true')).toHaveLength (3)
|
|
})
|
|
|
|
it ('keeps untouched original created values unchanged in the save payload', async () => {
|
|
let actions: DialogueFormAction[] = []
|
|
const row = buildPostImportRow ({
|
|
attributes: {
|
|
originalCreatedFrom: '2024-01-01T12:34+09:00',
|
|
originalCreatedBefore: '2024-01-01T12:35+09:00' } })
|
|
const controls: DialogueFormControls = {
|
|
close: vi.fn (),
|
|
confirm: vi.fn (),
|
|
setActions: next => {
|
|
actions = next
|
|
} }
|
|
const onSave = vi.fn ().mockResolvedValue ({ saved: true, row: null })
|
|
|
|
render (
|
|
<PostImportRowForm
|
|
row={row}
|
|
controls={controls}
|
|
onSave={onSave}/>)
|
|
await waitFor (() => expect (actions.length).toBe (2))
|
|
|
|
await act (async () => {
|
|
void actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
|
|
})
|
|
|
|
expect (onSave).toHaveBeenCalledWith ({
|
|
draft: expect.objectContaining ({
|
|
originalCreatedFrom: '2024-01-01T12:34+09:00',
|
|
originalCreatedBefore: '2024-01-01T12:35+09:00' }),
|
|
resetRequested: false })
|
|
})
|
|
|
|
it (
|
|
'shows upload input only when the thumbnail URL is blank',
|
|
async () => {
|
|
let actions: DialogueFormAction[] = []
|
|
const controls: DialogueFormControls = {
|
|
close: vi.fn (),
|
|
confirm: vi.fn (),
|
|
setActions: next => {
|
|
actions = next
|
|
} }
|
|
const onSave = vi.fn ().mockResolvedValue ({ saved: true, row: null })
|
|
|
|
const { container } = render (
|
|
<PostImportRowForm
|
|
row={buildPostImportRow ({
|
|
attributes: { duration: '2', tags: 'tag1' } })}
|
|
controls={controls}
|
|
onSave={onSave}/>)
|
|
await waitFor (() => expect (actions.length).toBe (2))
|
|
|
|
const labels = Array.from (container.querySelectorAll ('label'))
|
|
.map (node => node.textContent?.trim ())
|
|
|
|
expect (labels.slice (0, 6)).toEqual ([
|
|
'URL',
|
|
'サムネール',
|
|
'タイトル',
|
|
'オリジナルの作成日時',
|
|
'タグ',
|
|
'親投稿'])
|
|
expect (container.querySelector ('input[type="file"]')).toHaveAttribute (
|
|
'accept',
|
|
'image/*')
|
|
expect (screen.queryByPlaceholderText ('例: 2 / 2.5 / 1:23')).not.toBeInTheDocument ()
|
|
expect (screen.getByDisplayValue ('tag1')).toBeInTheDocument ()
|
|
|
|
await act (async () => {
|
|
await actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
|
|
})
|
|
|
|
expect (onSave).toHaveBeenCalledWith ({
|
|
draft: expect.objectContaining ({
|
|
tags: 'tag1' }),
|
|
resetRequested: false })
|
|
})
|
|
|
|
it ('keeps reset enabled when the value matches but provenance still differs', async () => {
|
|
const row = buildPostImportRow ({
|
|
attributes: { title: 'same title' },
|
|
provenance: { title: 'manual' },
|
|
resetSnapshot: {
|
|
url: 'https://example.com/post',
|
|
attributes: {
|
|
title: 'same title',
|
|
thumbnailBase: '',
|
|
originalCreatedFrom: '',
|
|
originalCreatedBefore: '',
|
|
tags: '',
|
|
parentPostIds: '' },
|
|
provenance: {
|
|
url: 'manual',
|
|
title: 'automatic',
|
|
thumbnailBase: 'automatic',
|
|
originalCreatedFrom: 'automatic',
|
|
originalCreatedBefore: 'automatic',
|
|
tags: 'automatic',
|
|
parentPostIds: 'automatic' },
|
|
tagSources: { automatic: '', manual: '' },
|
|
fieldWarnings: { },
|
|
baseWarnings: [] } })
|
|
let actions: DialogueFormAction[] = []
|
|
|
|
render (
|
|
<PostImportRowForm
|
|
row={row}
|
|
controls={{
|
|
close: vi.fn (),
|
|
confirm: vi.fn (),
|
|
setActions: next => {
|
|
actions = next
|
|
} }}
|
|
onSave={vi.fn ()}/>)
|
|
await waitFor (() => expect (actions.length).toBe (2))
|
|
|
|
expect (actions.find (action => action.label === '変更をリセット')?.disabled).toBe (false)
|
|
})
|
|
|
|
it (
|
|
'disables every field while save validation is pending and re-enables them afterwards',
|
|
async () => {
|
|
let actions: DialogueFormAction[] = []
|
|
let resolveSave:
|
|
((value: { saved: boolean
|
|
row: ReturnType<typeof buildPostImportRow> | null }) => void) | null
|
|
= null
|
|
const onSave = vi.fn (() =>
|
|
new Promise<{ saved: boolean
|
|
row: ReturnType<typeof buildPostImportRow> | null }> (resolve => {
|
|
resolveSave = resolve
|
|
}))
|
|
|
|
render (
|
|
<PostImportRowForm
|
|
row={buildPostImportRow ({ attributes: { title: 'draft title' } })}
|
|
controls={{
|
|
close: vi.fn (),
|
|
confirm: vi.fn (),
|
|
setActions: next => {
|
|
actions = next
|
|
} }}
|
|
onSave={onSave}/>)
|
|
await waitFor (() => expect (actions.length).toBe (2))
|
|
|
|
let savePromise: Promise<boolean | void> | undefined
|
|
await act (async () => {
|
|
savePromise = actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
|
|
})
|
|
|
|
await waitFor (() => {
|
|
screen.getAllByRole ('textbox').forEach (textbox => {
|
|
expect (textbox).toBeDisabled ()
|
|
})
|
|
})
|
|
|
|
resolveSave?.({
|
|
saved: false,
|
|
row: buildPostImportRow ({
|
|
attributes: { title: 'draft title' },
|
|
validationErrors: { title: ['タイトルを確認してください.'] } }) })
|
|
await act (async () => {
|
|
await savePromise
|
|
})
|
|
|
|
await waitFor (() => {
|
|
screen.getAllByRole ('textbox').forEach (textbox => {
|
|
expect (textbox).not.toBeDisabled ()
|
|
})
|
|
})
|
|
expect (screen.getByDisplayValue ('draft title')).toBeInTheDocument ()
|
|
expect (screen.getByText ('タイトルを確認してください.')).toBeInTheDocument ()
|
|
})
|
|
})
|