mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 11:37:29 +08:00
fix(workflow): align share response contracts (#708)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import type { WorkflowShare } from '@/api/types'
|
||||
import WorkflowShareCard from '@/components/cards/WorkflowShareCard.vue'
|
||||
import { screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
openSharedDialog: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useSharedDialog', () => ({
|
||||
openSharedDialog: (...args: unknown[]) => mocks.openSharedDialog(...args),
|
||||
}))
|
||||
|
||||
function createWorkflowShare(overrides: Partial<WorkflowShare> = {}): WorkflowShare {
|
||||
return {
|
||||
date: '2026-08-23 12:00:00',
|
||||
id: '91',
|
||||
share_comment: '用于验证工作流分享卡片',
|
||||
share_title: '分享工作流',
|
||||
share_user: '测试用户',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
async function renderCard(workflow = createWorkflowShare()) {
|
||||
const events = {
|
||||
delete: vi.fn(),
|
||||
update: vi.fn(),
|
||||
}
|
||||
const result = await renderWithProviders(WorkflowShareCard, {
|
||||
props: {
|
||||
eventTypes: [{ title: '下载完成', value: 'download_complete' }],
|
||||
onDelete: events.delete,
|
||||
onUpdate: events.update,
|
||||
workflow,
|
||||
},
|
||||
})
|
||||
|
||||
return { ...result, events, workflow }
|
||||
}
|
||||
|
||||
describe('WorkflowShareCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('opens the fork dialog with the current workflow and closes on terminal events', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { workflow } = await renderCard()
|
||||
|
||||
await user.click(screen.getByText('分享工作流'))
|
||||
|
||||
expect(mocks.openSharedDialog).toHaveBeenCalledOnce()
|
||||
expect(mocks.openSharedDialog.mock.calls[0][1]).toEqual({
|
||||
eventTypes: [{ title: '下载完成', value: 'download_complete' }],
|
||||
workflow,
|
||||
})
|
||||
expect(mocks.openSharedDialog.mock.calls[0][3]).toEqual({ closeOn: ['close', 'fork', 'delete'] })
|
||||
})
|
||||
|
||||
it('renders usage and date metadata with an ID-derived gradient', async () => {
|
||||
const { container } = await renderCard(createWorkflowShare({ count: 1234 }))
|
||||
|
||||
expect(screen.getByText('1,234')).toBeInTheDocument()
|
||||
expect(container.querySelector('.absolute.right-0.bottom-0')).not.toHaveTextContent('')
|
||||
expect(container.querySelector('.workflow-share-card')?.getAttribute('style')).toContain('linear-gradient')
|
||||
})
|
||||
|
||||
it('uses a fallback gradient and omits optional metadata when identity and usage are absent', async () => {
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.25)
|
||||
const { container } = await renderCard(createWorkflowShare({ count: undefined, date: undefined, id: undefined }))
|
||||
|
||||
expect(screen.queryByText('1,234')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('.workflow-share-card')?.getAttribute('style')).toContain('linear-gradient')
|
||||
})
|
||||
|
||||
it('maps payload-free fork and delete completion to parent refresh events', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { events } = await renderCard()
|
||||
await user.click(screen.getByText('分享工作流'))
|
||||
const dialogEvents = mocks.openSharedDialog.mock.calls[0][2] as {
|
||||
delete: () => void
|
||||
fork: () => void
|
||||
}
|
||||
|
||||
dialogEvents.fork()
|
||||
dialogEvents.delete()
|
||||
|
||||
expect(events.update).toHaveBeenCalledOnce()
|
||||
expect(events.delete).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -78,10 +78,10 @@ async function doFork() {
|
||||
try {
|
||||
processing.value = true
|
||||
// 请求API
|
||||
const result = await api.post<{ id: string }>('workflow/fork', props.workflow, { feedback: 'silent' })
|
||||
await api.post('workflow/fork', props.workflow, { feedback: 'silent' })
|
||||
$toast.success(t('workflow.addSuccess', { name: props.workflow?.share_title }))
|
||||
// 完成
|
||||
emit('fork', result.id)
|
||||
emit('fork')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(
|
||||
@@ -103,7 +103,7 @@ async function doDelete() {
|
||||
try {
|
||||
deleting.value = true
|
||||
// 请求API
|
||||
const result = await api.delete<{ id: string }>(`workflow/share/${props.workflow?.id}`, {
|
||||
await api.delete(`workflow/share/${props.workflow?.id}`, {
|
||||
params: {
|
||||
share_uid: globalSettings.USER_UNIQUE_ID,
|
||||
},
|
||||
@@ -111,7 +111,7 @@ async function doDelete() {
|
||||
})
|
||||
$toast.success(t('workflow.cancelSuccess'))
|
||||
// 完成
|
||||
emit('delete', result.id)
|
||||
emit('delete')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$toast.error(t('workflow.cancelFailed', { message: error instanceof Error ? error.message : '' }))
|
||||
|
||||
@@ -1,19 +1,260 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { WorkflowShare } from '@/api/types'
|
||||
import ForkWorkflowDialog from '@/components/dialog/ForkWorkflowDialog.vue'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { HttpResponse, http } from 'msw'
|
||||
import { apiFailureJson, apiJson } from '@tests/support/msw/response'
|
||||
import { server } from '@tests/support/msw/server'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const dialogSource = readFileSync('src/components/dialog/ForkWorkflowDialog.vue', 'utf8')
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ForkWorkflowDialog preview contract', () => {
|
||||
it('uses the static summary without loading an interactive VueFlow canvas', () => {
|
||||
expect(dialogSource).toContain('WorkflowSummaryPreview')
|
||||
expect(dialogSource).not.toContain('@vue-flow/core')
|
||||
expect(dialogSource).not.toContain('import.meta.glob')
|
||||
expect(dialogSource).not.toContain('<VueFlow')
|
||||
vi.mock('@/api/nprogress', () => ({
|
||||
configureNProgress: vi.fn(),
|
||||
doneNProgress: vi.fn(),
|
||||
startNProgress: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||
}))
|
||||
|
||||
const DialogCloseButtonStub = defineComponent({
|
||||
name: 'VDialogCloseBtn',
|
||||
emits: ['click'],
|
||||
setup(_props, { emit }) {
|
||||
return () => h('button', { type: 'button', onClick: () => emit('click') }, '关闭')
|
||||
},
|
||||
})
|
||||
|
||||
const WorkflowSummaryPreviewStub = defineComponent({
|
||||
name: 'WorkflowSummaryPreview',
|
||||
props: {
|
||||
actions: Array,
|
||||
flows: Array,
|
||||
},
|
||||
setup(props) {
|
||||
return () => h('div', { 'data-testid': 'workflow-preview' }, `${props.actions?.length}:${props.flows?.length}`)
|
||||
},
|
||||
})
|
||||
|
||||
function createWorkflowShare(overrides: Partial<WorkflowShare> = {}): WorkflowShare {
|
||||
return {
|
||||
actions: [{ id: 'action-1' }],
|
||||
flows: [{ id: 'flow-1' }],
|
||||
id: '91',
|
||||
share_comment: '用于验证工作流分享契约',
|
||||
share_title: '分享工作流',
|
||||
share_uid: 'owner-id',
|
||||
share_user: '测试用户',
|
||||
trigger_type: 'manual',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createDeferred() {
|
||||
let resolve!: () => void
|
||||
const promise = new Promise<void>(done => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
async function renderDialog(workflow = createWorkflowShare(), settings: Record<string, unknown> = {}) {
|
||||
const events = {
|
||||
close: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
fork: vi.fn(),
|
||||
}
|
||||
const result = await renderWithProviders(ForkWorkflowDialog, {
|
||||
global: {
|
||||
components: {
|
||||
VDialogCloseBtn: DialogCloseButtonStub,
|
||||
},
|
||||
stubs: {
|
||||
WorkflowSummaryPreview: WorkflowSummaryPreviewStub,
|
||||
},
|
||||
},
|
||||
initialState: {
|
||||
globalSettings: {
|
||||
data: {
|
||||
USER_UNIQUE_ID: 'owner-id',
|
||||
WORKFLOW_SHARE_MANAGE: false,
|
||||
...settings,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
eventTypes: [{ title: '下载完成', value: 'download_complete' }],
|
||||
modelValue: true,
|
||||
onClose: events.close,
|
||||
onDelete: events.delete,
|
||||
onFork: events.fork,
|
||||
workflow,
|
||||
},
|
||||
})
|
||||
|
||||
it('reserves the close-button safe area on small screens', () => {
|
||||
expect(dialogSource).toMatch(
|
||||
/@media screen and \(width <= 600px\)[\s\S]*?\.workflow-share-layout\s*\{[\s\S]*?padding-block-start:\s*2rem/,
|
||||
return { ...result, events, workflow }
|
||||
}
|
||||
|
||||
describe('ForkWorkflowDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it('renders parsed workflow data through the static summary preview', async () => {
|
||||
await renderDialog(
|
||||
createWorkflowShare({
|
||||
actions: JSON.stringify([{ id: 'action-1' }, { id: 'action-2' }]) as unknown as WorkflowShare['actions'],
|
||||
flows: JSON.stringify([{ id: 'flow-1' }]) as unknown as WorkflowShare['flows'],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toHaveTextContent('2:1')
|
||||
expect(document.querySelector('.workflow-share-layout')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['timer', { timer: '0 8 * * *', trigger_type: 'timer' }, '0 8 * * *'],
|
||||
['legacy timer', { timer: '0 9 * * *', trigger_type: undefined }, '0 9 * * *'],
|
||||
['known event', { event_type: 'download_complete', trigger_type: 'event' }, '下载完成'],
|
||||
['unknown event', { event_type: 'custom_event', trigger_type: 'event' }, 'custom_event'],
|
||||
['manual', { trigger_type: 'manual' }, '手动触发'],
|
||||
])('renders the %s trigger contract', async (_case, overrides, expected) => {
|
||||
await renderDialog(createWorkflowShare(overrides))
|
||||
|
||||
expect(screen.getByText(expected)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('falls back to an empty static preview when serialized workflow data is malformed', async () => {
|
||||
await renderDialog(
|
||||
createWorkflowShare({
|
||||
actions: '{invalid' as unknown as WorkflowShare['actions'],
|
||||
flows: 'not-json' as unknown as WorkflowShare['flows'],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toHaveTextContent('0:0')
|
||||
expect(console.error).toHaveBeenCalledWith('解析工作流数据失败:', expect.any(SyntaxError))
|
||||
})
|
||||
|
||||
it('keeps the fork action pending and completes a Response[None] success without reading data.id', async () => {
|
||||
const deferred = createDeferred()
|
||||
const requested = vi.fn(async (payload: unknown) => {
|
||||
await deferred.promise
|
||||
return payload
|
||||
})
|
||||
server.use(
|
||||
http.post('/api/v1/workflow/fork', async ({ request }) => {
|
||||
await requested(await request.json())
|
||||
return apiJson(null)
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
const { events, workflow } = await renderDialog()
|
||||
const button = screen.getByRole('button', { name: '复用工作流' })
|
||||
|
||||
await user.click(button)
|
||||
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
expect(button).toBeDisabled()
|
||||
expect(events.fork).not.toHaveBeenCalled()
|
||||
|
||||
deferred.resolve()
|
||||
await waitFor(() => expect(events.fork).toHaveBeenCalledOnce())
|
||||
expect(events.fork).toHaveBeenCalledWith()
|
||||
expect(requested).toHaveBeenCalledWith(workflow)
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('复用 分享工作流 成功!')
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports fork business and HTTP failures without emitting completion', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { events, rerender } = await renderDialog(createWorkflowShare({ share_title: '拒绝复用' }))
|
||||
|
||||
server.use(http.post('/api/v1/workflow/fork', () => apiFailureJson('工作流无效')))
|
||||
await user.click(screen.getByRole('button', { name: '复用工作流' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('复用 拒绝复用 失败:工作流无效'))
|
||||
expect(events.fork).not.toHaveBeenCalled()
|
||||
|
||||
mocks.toastError.mockClear()
|
||||
server.use(http.post('/api/v1/workflow/fork', () => HttpResponse.json({}, { status: 500 })))
|
||||
await rerender({ workflow: createWorkflowShare({ share_title: '网络失败' }) })
|
||||
await user.click(screen.getByRole('button', { name: '复用工作流' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(expect.stringContaining('复用 网络失败 失败')))
|
||||
expect(events.fork).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('button', { name: '复用工作流' })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['the owner', 'owner-id', false, true],
|
||||
['a share manager', 'other-user', true, true],
|
||||
['another ordinary user', 'other-user', false, false],
|
||||
])('shows delete permission for %s', async (_case, shareUid, canManage, visible) => {
|
||||
await renderDialog(createWorkflowShare({ share_uid: shareUid }), { WORKFLOW_SHARE_MANAGE: canManage })
|
||||
|
||||
if (visible) expect(screen.getByRole('button', { name: '取消分享' })).toBeInTheDocument()
|
||||
else expect(screen.queryByRole('button', { name: '取消分享' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('deletes the exact share and completes a Response[None] success without reading data.id', async () => {
|
||||
const deferred = createDeferred()
|
||||
const requested = vi.fn(async (url: URL) => {
|
||||
await deferred.promise
|
||||
return url
|
||||
})
|
||||
server.use(
|
||||
http.delete('/api/v1/workflow/share/:id', async ({ request }) => {
|
||||
await requested(new URL(request.url))
|
||||
return apiJson(null)
|
||||
}),
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
const { events } = await renderDialog()
|
||||
const button = screen.getByRole('button', { name: '取消分享' })
|
||||
|
||||
await user.click(button)
|
||||
await waitFor(() => expect(requested).toHaveBeenCalledOnce())
|
||||
expect(button).toBeDisabled()
|
||||
expect(events.delete).not.toHaveBeenCalled()
|
||||
|
||||
deferred.resolve()
|
||||
await waitFor(() => expect(events.delete).toHaveBeenCalledOnce())
|
||||
expect(events.delete).toHaveBeenCalledWith()
|
||||
expect(requested.mock.calls[0][0].pathname).toBe('/api/v1/workflow/share/91')
|
||||
expect(requested.mock.calls[0][0].searchParams.get('share_uid')).toBe('owner-id')
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('取消分享成功')
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports delete business and HTTP failures without emitting completion', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { events } = await renderDialog()
|
||||
|
||||
server.use(http.delete('/api/v1/workflow/share/:id', () => apiFailureJson('没有删除权限')))
|
||||
await user.click(screen.getByRole('button', { name: '取消分享' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('取消分享失败:没有删除权限'))
|
||||
expect(events.delete).not.toHaveBeenCalled()
|
||||
|
||||
mocks.toastError.mockClear()
|
||||
server.use(http.delete('/api/v1/workflow/share/:id', () => HttpResponse.json({}, { status: 500 })))
|
||||
await user.click(screen.getByRole('button', { name: '取消分享' }))
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(expect.stringContaining('取消分享失败')))
|
||||
expect(events.delete).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('button', { name: '取消分享' })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('emits close from the dialog close control', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { events } = await renderDialog()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '关闭' }))
|
||||
|
||||
expect(events.close).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import type { Workflow } from '@/api/types'
|
||||
import WorkflowShareDialog from '@/components/dialog/WorkflowShareDialog.vue'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { HttpResponse, http } from 'msw'
|
||||
import { apiFailureJson, apiJson } from '@tests/support/msw/response'
|
||||
import { server } from '@tests/support/msw/server'
|
||||
import { renderWithProviders } from '@tests/support/render'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('vue-toastification', () => ({
|
||||
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
|
||||
}))
|
||||
|
||||
const DialogCloseButtonStub = defineComponent({
|
||||
name: 'VDialogCloseBtn',
|
||||
emits: ['click'],
|
||||
setup(_props, { emit }) {
|
||||
return () => h('button', { type: 'button', onClick: () => emit('click') }, '关闭')
|
||||
},
|
||||
})
|
||||
|
||||
function createDeferred() {
|
||||
let resolve!: () => void
|
||||
const promise = new Promise<void>(done => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
async function renderDialog(workflow: Workflow | null = { id: '81', name: '待分享工作流' }) {
|
||||
const close = vi.fn()
|
||||
const result = await renderWithProviders(WorkflowShareDialog, {
|
||||
global: {
|
||||
components: {
|
||||
VDialogCloseBtn: DialogCloseButtonStub,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
modelValue: true,
|
||||
onClose: close,
|
||||
workflow: workflow ?? undefined,
|
||||
},
|
||||
})
|
||||
|
||||
return { ...result, close, workflow }
|
||||
}
|
||||
|
||||
async function fillRequiredFields(comment = '覆盖工作流分享契约', shareUser = '测试分享人') {
|
||||
const user = userEvent.setup()
|
||||
await user.type(screen.getByLabelText('说明'), comment)
|
||||
await user.type(screen.getByLabelText('分享用户'), shareUser)
|
||||
return user
|
||||
}
|
||||
|
||||
describe('WorkflowShareDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['description', '', '测试分享人'],
|
||||
['sharing user', '覆盖工作流分享契约', ''],
|
||||
])('blocks submission when the %s is missing', async (_case, comment, shareUser) => {
|
||||
const requested = vi.fn()
|
||||
server.use(
|
||||
http.post('/api/v1/workflow/share', () => {
|
||||
requested()
|
||||
return apiJson(null)
|
||||
}),
|
||||
)
|
||||
await renderDialog()
|
||||
const user = userEvent.setup()
|
||||
|
||||
if (comment) await user.type(screen.getByLabelText('说明'), comment)
|
||||
if (shareUser) await user.type(screen.getByLabelText('分享用户'), shareUser)
|
||||
await user.click(screen.getByRole('button', { name: '确认分享' }))
|
||||
|
||||
expect(requested).not.toHaveBeenCalled()
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('submits the exact payload once, stays pending, and closes after success', async () => {
|
||||
const deferred = createDeferred()
|
||||
const payloads: unknown[] = []
|
||||
server.use(
|
||||
http.post('/api/v1/workflow/share', async ({ request }) => {
|
||||
payloads.push(await request.json())
|
||||
await deferred.promise
|
||||
return apiJson(null)
|
||||
}),
|
||||
)
|
||||
const { close, workflow } = await renderDialog()
|
||||
const expectedWorkflow = workflow!
|
||||
const user = await fillRequiredFields('只提交分享字段', '分享者甲')
|
||||
const button = screen.getByRole('button', { name: '确认分享' })
|
||||
|
||||
await user.click(button)
|
||||
await waitFor(() => expect(payloads).toHaveLength(1))
|
||||
expect(payloads[0]).toEqual({
|
||||
id: expectedWorkflow.id,
|
||||
share_comment: '只提交分享字段',
|
||||
share_title: expectedWorkflow.name,
|
||||
share_user: '分享者甲',
|
||||
})
|
||||
expect(button).toBeDisabled()
|
||||
expect(close).not.toHaveBeenCalled()
|
||||
|
||||
deferred.resolve()
|
||||
await waitFor(() => expect(close).toHaveBeenCalledOnce())
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('待分享工作流 分享成功!')
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the dialog retryable after business and HTTP failures', async () => {
|
||||
const { close } = await renderDialog()
|
||||
const user = await fillRequiredFields()
|
||||
const button = screen.getByRole('button', { name: '确认分享' })
|
||||
|
||||
server.use(http.post('/api/v1/workflow/share', () => apiFailureJson('远端拒绝')))
|
||||
await user.click(button)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('待分享工作流 分享失败:远端拒绝!'))
|
||||
expect(close).not.toHaveBeenCalled()
|
||||
expect(button).not.toBeDisabled()
|
||||
|
||||
mocks.toastError.mockClear()
|
||||
server.use(http.post('/api/v1/workflow/share', () => HttpResponse.json({}, { status: 500 })))
|
||||
await user.click(button)
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith(expect.stringContaining('待分享工作流 分享失败')))
|
||||
expect(close).not.toHaveBeenCalled()
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('tracks a replaced workflow in the submitted share identity', async () => {
|
||||
const payloads: unknown[] = []
|
||||
server.use(
|
||||
http.post('/api/v1/workflow/share', async ({ request }) => {
|
||||
payloads.push(await request.json())
|
||||
return apiJson(null)
|
||||
}),
|
||||
)
|
||||
const { rerender } = await renderDialog(null)
|
||||
expect(screen.getByLabelText('标题')).toHaveValue('')
|
||||
await rerender({ workflow: { id: '82', name: '替换后的工作流' } })
|
||||
const user = await fillRequiredFields()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '确认分享' }))
|
||||
|
||||
await waitFor(() => expect(payloads).toHaveLength(1))
|
||||
expect(payloads[0]).toMatchObject({ id: '82', share_title: '替换后的工作流' })
|
||||
})
|
||||
|
||||
it('emits close from the dialog close control', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { close } = await renderDialog()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '关闭' }))
|
||||
|
||||
expect(close).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -404,6 +404,9 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
||||
'src/components/cards/PluginCard.vue',
|
||||
'src/components/cards/PluginAppCard.vue',
|
||||
'src/components/cards/WorkflowTaskCard.vue',
|
||||
'src/components/cards/WorkflowShareCard.vue',
|
||||
'src/components/dialog/ForkWorkflowDialog.vue',
|
||||
'src/components/dialog/WorkflowShareDialog.vue',
|
||||
'src/components/dialog/PluginMarketDetailDialog.vue',
|
||||
'src/components/dialog/PluginMarketSettingDialog.vue',
|
||||
'src/components/dialog/PluginVersionHistoryDialog.vue',
|
||||
@@ -555,6 +558,24 @@ export default defineConfig(({ command, mode, isPreview }) => ({
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
},
|
||||
'src/components/cards/WorkflowShareCard.vue': {
|
||||
branches: 75,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
},
|
||||
'src/components/dialog/ForkWorkflowDialog.vue': {
|
||||
branches: 75,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
},
|
||||
'src/components/dialog/WorkflowShareDialog.vue': {
|
||||
branches: 75,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
},
|
||||
'src/components/dialog/UserAddEditDialog.vue': {
|
||||
branches: 75,
|
||||
functions: 80,
|
||||
|
||||
Reference in New Issue
Block a user