362 lines
11 KiB
JavaScript
362 lines
11 KiB
JavaScript
import React from 'react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import StudioUploadQueue from '../StudioUploadQueue'
|
|
|
|
let pageMock = { props: {} }
|
|
|
|
vi.mock('@inertiajs/react', () => ({
|
|
usePage: () => pageMock,
|
|
}))
|
|
|
|
vi.mock('../../../Layouts/StudioLayout', () => ({
|
|
default: ({ children }) => <div>{children}</div>,
|
|
}))
|
|
|
|
function makeQueueProps(overrides = {}) {
|
|
return {
|
|
title: 'Upload Queue',
|
|
description: 'Queue drafts',
|
|
chunkSize: 5242880,
|
|
chunkRequestTimeoutMs: 45000,
|
|
contentTypes: [
|
|
{
|
|
name: 'Photography',
|
|
categories: [
|
|
{ id: 10, name: 'Portraits', children: [] },
|
|
],
|
|
},
|
|
],
|
|
queue: {
|
|
filters: { batch_id: 1, status: 'all', sort: 'newest' },
|
|
batches: [{ id: 1, name: 'Spring Set' }],
|
|
current_batch: {
|
|
id: 1,
|
|
name: 'Spring Set',
|
|
status: 'completed_with_errors',
|
|
total_items: 2,
|
|
ready_items: 1,
|
|
processing_items: 0,
|
|
needs_review_items: 1,
|
|
failed_items: 0,
|
|
updated_at: '2026-04-18T10:00:00Z',
|
|
},
|
|
status_options: [
|
|
{ value: 'all', label: 'All' },
|
|
{ value: 'ready', label: 'Ready' },
|
|
{ value: 'needs_review', label: 'Needs review' },
|
|
],
|
|
sort_options: [
|
|
{ value: 'newest', label: 'Newest first' },
|
|
{ value: 'filename', label: 'Filename' },
|
|
],
|
|
items: [
|
|
{
|
|
id: 101,
|
|
title: 'Ready draft',
|
|
original_filename: 'ready.webp',
|
|
status: 'ready',
|
|
processing_stage: 'finalized',
|
|
metadata_label: '100% complete',
|
|
is_ready_to_publish: true,
|
|
missing: [],
|
|
error_message: null,
|
|
updated_at: '2026-04-18T10:00:00Z',
|
|
edit_url: '/studio/artworks/1/edit',
|
|
actions: {
|
|
can_edit: true,
|
|
can_publish: true,
|
|
can_delete: true,
|
|
can_retry_processing: false,
|
|
can_generate_ai: true,
|
|
},
|
|
},
|
|
{
|
|
id: 102,
|
|
title: 'Needs review draft',
|
|
original_filename: 'review.webp',
|
|
status: 'needs_review',
|
|
processing_stage: 'finalized',
|
|
metadata_label: '75% complete',
|
|
is_ready_to_publish: false,
|
|
missing: ['Needs maturity review'],
|
|
error_message: null,
|
|
updated_at: '2026-04-18T11:00:00Z',
|
|
edit_url: '/studio/artworks/2/edit',
|
|
actions: {
|
|
can_edit: true,
|
|
can_publish: false,
|
|
can_delete: true,
|
|
can_retry_processing: false,
|
|
can_generate_ai: true,
|
|
},
|
|
},
|
|
],
|
|
...overrides.queue,
|
|
},
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('StudioUploadQueue', () => {
|
|
beforeEach(() => {
|
|
pageMock = { props: makeQueueProps() }
|
|
window.axios = {
|
|
get: vi.fn().mockResolvedValue({ data: pageMock.props.queue }),
|
|
post: vi.fn().mockResolvedValue({ data: { success: 1, failed: 0, errors: [] } }),
|
|
}
|
|
window.confirm = vi.fn(() => true)
|
|
window.prompt = vi.fn(() => 'DELETE')
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.useRealTimers()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('renders mixed queue states and item actions', () => {
|
|
render(<StudioUploadQueue />)
|
|
|
|
expect(screen.getByText('Ready draft')).not.toBeNull()
|
|
expect(screen.getByText('Needs review draft')).not.toBeNull()
|
|
expect(screen.getAllByText('Ready to publish')[0]).not.toBeNull()
|
|
expect(screen.getByText('Needs maturity review')).not.toBeNull()
|
|
expect(screen.getAllByRole('link', { name: /edit in studio/i })).toHaveLength(2)
|
|
expect(screen.getAllByRole('button', { name: /^generate ai$/i })).toHaveLength(3)
|
|
})
|
|
|
|
it('reloads the queue when filters change', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(<StudioUploadQueue />)
|
|
|
|
await user.selectOptions(screen.getByRole('combobox', { name: /filter/i }), 'ready')
|
|
|
|
await waitFor(() => {
|
|
expect(window.axios.get).toHaveBeenCalledWith('/api/studio/upload-queue', {
|
|
params: expect.objectContaining({
|
|
batch_id: 1,
|
|
status: 'ready',
|
|
sort: 'newest',
|
|
}),
|
|
})
|
|
})
|
|
})
|
|
|
|
it('shows a publish confirmation summary before bulk publish', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(<StudioUploadQueue />)
|
|
|
|
const checkboxes = screen.getAllByRole('checkbox')
|
|
const itemCheckboxes = checkboxes.slice(-2)
|
|
await user.click(itemCheckboxes[0])
|
|
await user.click(itemCheckboxes[1])
|
|
await user.click(screen.getByRole('button', { name: /publish selected/i }))
|
|
|
|
expect(window.confirm).toHaveBeenCalledWith([
|
|
'Publish 1 ready draft(s)?',
|
|
'Selected: 2',
|
|
'Ready now: 1',
|
|
'Blocked and skipped: 1',
|
|
'Needs review: 1',
|
|
'Blocked drafts will not be published.',
|
|
].join('\n'))
|
|
|
|
await waitFor(() => {
|
|
expect(window.axios.post).toHaveBeenCalledWith('/api/studio/upload-queue/bulk', expect.objectContaining({
|
|
action: 'publish',
|
|
item_ids: [101, 102],
|
|
}))
|
|
})
|
|
})
|
|
|
|
it('does not attempt bulk publish when no selected drafts are ready', async () => {
|
|
const user = userEvent.setup()
|
|
pageMock = {
|
|
props: makeQueueProps({
|
|
queue: {
|
|
items: [
|
|
{
|
|
id: 202,
|
|
title: 'Blocked draft',
|
|
original_filename: 'blocked.webp',
|
|
status: 'needs_metadata',
|
|
processing_stage: 'finalized',
|
|
metadata_label: '50% complete',
|
|
is_ready_to_publish: false,
|
|
missing: ['Missing title'],
|
|
error_message: null,
|
|
updated_at: '2026-04-18T10:00:00Z',
|
|
edit_url: '/studio/artworks/3/edit',
|
|
actions: {
|
|
can_edit: true,
|
|
can_publish: false,
|
|
can_delete: true,
|
|
can_retry_processing: false,
|
|
can_generate_ai: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
}
|
|
|
|
render(<StudioUploadQueue />)
|
|
|
|
const checkboxes = screen.getAllByRole('checkbox')
|
|
await user.click(checkboxes.at(-1))
|
|
await user.click(screen.getByRole('button', { name: /publish selected/i }))
|
|
|
|
expect(window.confirm).not.toHaveBeenCalled()
|
|
expect(window.axios.post).not.toHaveBeenCalledWith('/api/studio/upload-queue/bulk', expect.objectContaining({
|
|
action: 'publish',
|
|
}))
|
|
expect(screen.getByText('None of the selected drafts are ready to publish yet.')).not.toBeNull()
|
|
})
|
|
|
|
it('shows the correct Studio links and publish readiness state per item', () => {
|
|
render(<StudioUploadQueue />)
|
|
|
|
const studioLinks = screen.getAllByRole('link', { name: /edit in studio/i })
|
|
expect(studioLinks).toHaveLength(2)
|
|
expect(studioLinks[0].getAttribute('href')).toBe('/studio/artworks/1/edit')
|
|
expect(studioLinks[1].getAttribute('href')).toBe('/studio/artworks/2/edit')
|
|
expect(screen.getAllByRole('button', { name: /^publish$/i })).toHaveLength(1)
|
|
})
|
|
|
|
it('bulk actions apply only to selected queue items', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(<StudioUploadQueue />)
|
|
|
|
const checkboxes = screen.getAllByRole('checkbox')
|
|
const itemCheckboxes = checkboxes.slice(-2)
|
|
await user.click(itemCheckboxes[0])
|
|
await user.click(screen.getAllByRole('button', { name: /^generate ai$/i })[0])
|
|
|
|
await waitFor(() => {
|
|
expect(window.axios.post).toHaveBeenCalledWith('/api/studio/upload-queue/bulk', expect.objectContaining({
|
|
action: 'generate_ai',
|
|
item_ids: [101],
|
|
}))
|
|
})
|
|
})
|
|
|
|
it('shows failed items clearly and lets creators retry them', async () => {
|
|
const user = userEvent.setup()
|
|
pageMock = {
|
|
props: makeQueueProps({
|
|
queue: {
|
|
current_batch: {
|
|
id: 1,
|
|
name: 'Spring Set',
|
|
status: 'completed_with_errors',
|
|
total_items: 1,
|
|
ready_items: 0,
|
|
processing_items: 0,
|
|
needs_review_items: 0,
|
|
failed_items: 1,
|
|
updated_at: '2026-04-18T12:00:00Z',
|
|
},
|
|
items: [
|
|
{
|
|
id: 303,
|
|
title: 'Broken draft',
|
|
original_filename: 'broken.webp',
|
|
status: 'failed',
|
|
processing_stage: 'finalized',
|
|
metadata_label: '25% complete',
|
|
is_ready_to_publish: false,
|
|
missing: ['Processing incomplete'],
|
|
error_message: 'Derivative generation failed.',
|
|
updated_at: '2026-04-18T12:00:00Z',
|
|
edit_url: '/studio/artworks/4/edit',
|
|
actions: {
|
|
can_edit: true,
|
|
can_publish: false,
|
|
can_delete: true,
|
|
can_retry_processing: true,
|
|
can_generate_ai: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
}
|
|
|
|
render(<StudioUploadQueue />)
|
|
|
|
expect(screen.getByText('Derivative generation failed.')).not.toBeNull()
|
|
expect(screen.getByText('Processing incomplete')).not.toBeNull()
|
|
|
|
await user.click(screen.getByRole('button', { name: /retry/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(window.axios.post).toHaveBeenCalledWith('/api/studio/upload-queue/items/303/retry')
|
|
})
|
|
})
|
|
|
|
it('polls the queue while processing items are still running', async () => {
|
|
vi.useFakeTimers()
|
|
pageMock = {
|
|
props: makeQueueProps({
|
|
queue: {
|
|
current_batch: {
|
|
id: 1,
|
|
name: 'Spring Set',
|
|
status: 'processing',
|
|
total_items: 1,
|
|
ready_items: 0,
|
|
processing_items: 1,
|
|
needs_review_items: 0,
|
|
failed_items: 0,
|
|
updated_at: '2026-04-18T12:15:00Z',
|
|
},
|
|
items: [
|
|
{
|
|
id: 404,
|
|
title: 'Processing draft',
|
|
original_filename: 'processing.webp',
|
|
status: 'processing',
|
|
processing_stage: 'maturity_check',
|
|
metadata_label: '50% complete',
|
|
is_ready_to_publish: false,
|
|
missing: ['Maturity analysis pending'],
|
|
error_message: null,
|
|
updated_at: '2026-04-18T12:15:00Z',
|
|
edit_url: '/studio/artworks/5/edit',
|
|
actions: {
|
|
can_edit: true,
|
|
can_publish: false,
|
|
can_delete: true,
|
|
can_retry_processing: false,
|
|
can_generate_ai: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
}
|
|
|
|
render(<StudioUploadQueue />)
|
|
|
|
expect(screen.getByText('Maturity analysis pending')).not.toBeNull()
|
|
expect(screen.queryByRole('button', { name: /^publish$/i })).toBeNull()
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(3000)
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(window.axios.get).toHaveBeenCalledWith('/api/studio/upload-queue', {
|
|
params: expect.objectContaining({
|
|
batch_id: 1,
|
|
status: 'all',
|
|
sort: 'newest',
|
|
}),
|
|
})
|
|
})
|
|
}) |